├── .copier-answers.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── TEST_FAIL_TEMPLATE.md ├── dependabot.yml ├── scripts │ └── download_data.sh └── workflows │ ├── build_docs.yml │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── _typos.toml ├── docs ├── api │ ├── common.md │ ├── hcs.md │ ├── images.md │ ├── ngio.md │ ├── tables.md │ └── utils.md ├── code_of_conduct.md ├── contributing.md ├── getting_started │ ├── 0_quickstart.md │ ├── 1_ome_zarr_containers.md │ ├── 2_images.md │ ├── 3_tables.md │ ├── 4_masked_images.md │ └── 5_hcs.md ├── index.md └── tutorials │ ├── feature_extraction.ipynb │ ├── hcs_processing.ipynb │ ├── image_processing.ipynb │ └── image_segmentation.ipynb ├── mkdocs.yml ├── pyproject.toml ├── src └── ngio │ ├── __init__.py │ ├── common │ ├── __init__.py │ ├── _array_pipe.py │ ├── _axes_transforms.py │ ├── _common_types.py │ ├── _dimensions.py │ ├── _masking_roi.py │ ├── _pyramid.py │ ├── _roi.py │ ├── _slicer.py │ └── _zoom.py │ ├── hcs │ ├── __init__.py │ └── plate.py │ ├── images │ ├── __init__.py │ ├── abstract_image.py │ ├── create.py │ ├── image.py │ ├── label.py │ ├── masked_image.py │ └── ome_zarr_container.py │ ├── ome_zarr_meta │ ├── __init__.py │ ├── _meta_handlers.py │ ├── ngio_specs │ │ ├── __init__.py │ │ ├── _axes.py │ │ ├── _channels.py │ │ ├── _dataset.py │ │ ├── _ngio_hcs.py │ │ ├── _ngio_image.py │ │ └── _pixel_size.py │ └── v04 │ │ ├── __init__.py │ │ ├── _custom_models.py │ │ └── _v04_spec_utils.py │ ├── tables │ ├── __init__.py │ ├── _validators.py │ ├── backends │ │ ├── __init__.py │ │ ├── _abstract_backend.py │ │ ├── _anndata_utils.py │ │ ├── _anndata_v1.py │ │ ├── _csv_v1.py │ │ ├── _json_v1.py │ │ ├── _table_backends.py │ │ └── _utils.py │ ├── tables_container.py │ └── v1 │ │ ├── __init__.py │ │ ├── _feature_table.py │ │ ├── _generic_table.py │ │ └── _roi_table.py │ └── utils │ ├── __init__.py │ ├── _datasets.py │ ├── _errors.py │ ├── _fractal_fsspec_store.py │ ├── _logger.py │ └── _zarr_utils.py └── tests ├── conftest.py ├── data └── v04 │ ├── images │ ├── test_image_c1yx.zarr │ │ ├── 0 │ │ │ └── .zarray │ │ ├── 1 │ │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── label │ │ │ ├── 0 │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ ├── test_image_cyx.zarr │ │ ├── 0 │ │ │ └── .zarray │ │ ├── 1 │ │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── label │ │ │ ├── 0 │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ ├── test_image_czyx.zarr │ │ ├── 0 │ │ │ └── .zarray │ │ ├── 1 │ │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── label │ │ │ ├── 0 │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ ├── test_image_tcyx.zarr │ │ ├── 0 │ │ │ └── .zarray │ │ ├── 1 │ │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── label │ │ │ ├── 0 │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ ├── test_image_tczyx.zarr │ │ ├── 0 │ │ │ └── .zarray │ │ ├── 1 │ │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── label │ │ │ ├── 0 │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ ├── test_image_tyx.zarr │ │ ├── 0 │ │ │ └── .zarray │ │ ├── 1 │ │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── label │ │ │ ├── 0 │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ ├── test_image_tzyx.zarr │ │ ├── 0 │ │ │ └── .zarray │ │ ├── 1 │ │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── label │ │ │ ├── 0 │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ ├── test_image_yx.zarr │ │ ├── 0 │ │ │ └── .zarray │ │ ├── 1 │ │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── label │ │ │ ├── 0 │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ └── test_image_zyx.zarr │ │ ├── 0 │ │ └── .zarray │ │ ├── 1 │ │ └── .zarray │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── labels │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── label │ │ ├── 0 │ │ └── .zarray │ │ ├── 1 │ │ └── .zarray │ │ ├── .zattrs │ │ └── .zgroup │ └── meta │ ├── base_ome_zarr_image_meta.json │ ├── base_ome_zarr_image_meta_wrong_axis_order.json │ ├── base_ome_zarr_label_meta.json │ ├── base_ome_zarr_well_meta.json │ └── ome_zarr_well_path_normalization_meta.json └── unit ├── common ├── test_dimensions.py ├── test_pyramid.py └── test_roi.py ├── hcs ├── test_plate.py └── test_well.py ├── images ├── test_create.py ├── test_images.py ├── test_masked_images.py └── test_omezarr_container.py ├── ome_zarr_meta ├── test_image_handler.py ├── test_unit_ngio_specs.py └── test_unit_v04_utils.py ├── tables ├── test_backends.py ├── test_backends_utils.py ├── test_feature_table.py ├── test_generic_table.py ├── test_masking_roi_table_v1.py ├── test_roi_table_v1.py ├── test_table_group.py └── test_validators.py └── utils ├── test_download_datasets.py └── test_zarr_utils.py /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Do not edit - changes here will be overwritten by Copier 2 | _commit: v1 3 | _src_path: gh:pydev-guide/pyrepo-copier 4 | author_email: lorenzo.cerrone@uzh.ch 5 | author_name: Lorenzo Cerrone 6 | github_username: lorenzocerrone 7 | mode: tooling 8 | module_name: ngio 9 | project_name: ngio 10 | project_short_description: Next Generation file format IO 11 | 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # GitHub syntax highlighting 2 | pixi.lock linguist-language=YAML linguist-generated=true 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * ngio version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/TEST_FAIL_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ env.TITLE }}" 3 | labels: [bug] 4 | --- 5 | The {{ workflow }} workflow failed on {{ date | date("YYYY-MM-DD HH:mm") }} UTC 6 | 7 | The most recent failing test was on {{ env.PLATFORM }} py{{ env.PYTHON }} 8 | with commit: {{ sha }} 9 | 10 | Full run: https://github.com/{{ repo }}/actions/runs/{{ env.RUN_ID }} 11 | 12 | (This post will be updated if another test fails, as long as this issue remains open.) 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | commit-message: 10 | prefix: "ci(dependabot):" 11 | -------------------------------------------------------------------------------- /.github/scripts/download_data.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # filepath: download_zarr_datasets.sh 3 | 4 | # Default download directory 5 | DOWNLOAD_DIR="data" 6 | 7 | # Create download directory if it doesn't exist 8 | mkdir -p "$DOWNLOAD_DIR" 9 | 10 | # Function to check MD5 hash 11 | check_md5() { 12 | local file="$1" 13 | local expected="$2" 14 | 15 | if [[ "$(uname)" == "Darwin" ]]; then 16 | # macOS 17 | actual=$(md5 -q "$file") 18 | else 19 | # Linux/Ubuntu 20 | actual=$(md5sum "$file" | awk '{print $1}') 21 | fi 22 | 23 | echo "Expected: $expected" 24 | echo "Actual: $actual" 25 | 26 | if [[ "$actual" == "$expected" ]]; then 27 | return 0 # Success 28 | else 29 | return 1 # Failure 30 | fi 31 | } 32 | 33 | # Function to download a file 34 | download_file() { 35 | local url="$1" 36 | local output="$2" 37 | 38 | echo "Downloading $url to $output..." 39 | 40 | if command -v curl &> /dev/null; then 41 | curl -L -o "$output" "$url" 42 | elif command -v wget &> /dev/null; then 43 | wget -O "$output" "$url" 44 | else 45 | echo "Error: Neither curl nor wget is available. Please install one of them." 46 | exit 1 47 | fi 48 | } 49 | 50 | # Function to process a dataset 51 | process_dataset() { 52 | local filename="$1" 53 | local url="$2" 54 | local expected_hash="$3" 55 | 56 | local file_path="$DOWNLOAD_DIR/$filename" 57 | 58 | echo "Processing $filename..." 59 | 60 | # Check if file exists and has the correct hash 61 | if [[ -f "$file_path" ]] && check_md5 "$file_path" "$expected_hash"; then 62 | echo "File exists and has the correct hash." 63 | else 64 | # File doesn't exist or has incorrect hash 65 | if [[ -f "$file_path" ]]; then 66 | echo "File exists but has incorrect hash. Redownloading..." 67 | else 68 | echo "File doesn't exist. Downloading..." 69 | fi 70 | 71 | download_file "$url" "$file_path" 72 | 73 | # Verify the downloaded file 74 | if check_md5 "$file_path" "$expected_hash"; then 75 | echo "Download successful and hash verified." 76 | else 77 | echo "Error: Downloaded file has incorrect hash." 78 | return 1 79 | fi 80 | fi 81 | 82 | echo "File is ready at $file_path" 83 | return 0 84 | } 85 | 86 | # Process the CardioMyocyte dataset 87 | process_dataset "20200812-CardiomyocyteDifferentiation14-Cycle1.zarr.zip" \ 88 | "https://zenodo.org/records/13305156/files/20200812-CardiomyocyteDifferentiation14-Cycle1.zarr.zip" \ 89 | "efc21fe8d4ea3abab76226d8c166452c" 90 | 91 | process_dataset "20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr.zip" \ 92 | "https://zenodo.org/records/13305316/files/20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr.zip" \ 93 | "3ed3ea898e0ed42d397da2e1dbe40750" 94 | # To add more datasets, add more calls to process_dataset like this: 95 | # process_dataset "filename.zip" "download_url" "expected_md5_hash" 96 | 97 | echo "All datasets processed." -------------------------------------------------------------------------------- /.github/workflows/build_docs.yml: -------------------------------------------------------------------------------- 1 | name: Build Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | 10 | jobs: 11 | download-test-ome-zarr: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Check and Download Artifacts 16 | run: | 17 | bash .github/scripts/download_data.sh 18 | 19 | deploy: 20 | name: Deploy Docs 21 | needs: download-test-ome-zarr 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: 🐍 Set up Python 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: "3.13" 33 | cache-dependency-path: "pyproject.toml" 34 | cache: "pip" 35 | 36 | - name: Install Dependencies 37 | run: | 38 | python -m pip install -U pip 39 | python -m pip install .[dev] 40 | python -m pip install .[docs] 41 | 42 | - name: Configure Git user 43 | run: | 44 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 45 | git config --local user.name "github-actions[bot]" 46 | 47 | - name: Deploy docs 48 | run: | 49 | VERSION=$(echo $GITHUB_REF | sed 's/refs\/tags\///' | sed 's/refs\/heads\///') 50 | echo "Deploying version $VERSION" 51 | # Check if the version is a stable release 52 | # Meaning that starts with "v" and contains only numbers and dots 53 | if [[ $GITHUB_REF == refs/tags/* ]] && [[ $VERSION =~ ^v[0-9.]+$ ]]; then 54 | mike deploy --push --update-aliases $VERSION stable 55 | mike set-default --push stable 56 | echo "Deployed stable version" 57 | else 58 | mike deploy --push dev 59 | mike set-default --push dev 60 | echo "Deployed development version" 61 | fi 62 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | pull_request: 10 | workflow_dispatch: 11 | schedule: 12 | # run every week (for --pre release tests) 13 | - cron: "0 0 * * 0" 14 | 15 | # cancel in-progress runs that use the same workflow and branch 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | check-manifest: 22 | # check-manifest is a tool that checks that all files in version control are 23 | # included in the sdist (unless explicitly excluded) 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - run: pipx run check-manifest 28 | 29 | 30 | download-test-ome-zarr: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Check and Download Artifacts 35 | run: | 36 | bash .github/scripts/download_data.sh 37 | - name: Upload Artifacts 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: data 41 | path: data 42 | 43 | 44 | test: 45 | name: ${{ matrix.platform }} (${{ matrix.python-version }}) 46 | needs: download-test-ome-zarr 47 | runs-on: ${{ matrix.platform }} 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | python-version: ["3.11", "3.12", "3.13"] 52 | platform: [ubuntu-latest, macos-latest, windows-latest] 53 | # platform: [ubuntu-latest, macos-latest] 54 | exclude: 55 | - python-version: "3.11" 56 | platform: windows-latest 57 | - python-version: "3.12" 58 | platform: windows-latest 59 | 60 | steps: 61 | - uses: actions/checkout@v4 62 | 63 | - uses: actions/download-artifact@v4 64 | with: 65 | name: data 66 | path: data 67 | 68 | - name: Check Artifacts 69 | run: | 70 | ls -l data 71 | 72 | - name: 🐍 Set up Python ${{ matrix.python-version }} 73 | uses: actions/setup-python@v5 74 | with: 75 | python-version: ${{ matrix.python-version }} 76 | cache-dependency-path: "pyproject.toml" 77 | cache: "pip" 78 | 79 | - name: Install Dependencies 80 | run: | 81 | python -m pip install -U pip 82 | # if running a cron job, we add the --pre flag to test against pre-releases 83 | python -m pip install .[test] ${{ github.event_name == 'schedule' && '--pre' || '' }} 84 | 85 | - name: 🧪 Run Tests 86 | run: pytest 87 | 88 | # If something goes wrong with --pre tests, we can open an issue in the repo 89 | - name: 📝 Report --pre Failures 90 | if: failure() && github.event_name == 'schedule' 91 | uses: JasonEtco/create-an-issue@v2 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | PLATFORM: ${{ matrix.platform }} 95 | PYTHON: ${{ matrix.python-version }} 96 | RUN_ID: ${{ github.run_id }} 97 | TITLE: "[test-bot] pip install --pre is failing" 98 | with: 99 | filename: .github/TEST_FAIL_TEMPLATE.md 100 | update_existing: true 101 | 102 | - name: Coverage 103 | if: ${{ matrix.python-version == '3.10' }} && ${{ matrix.platform == 'ubuntu-latest' }} 104 | uses: codecov/codecov-action@v5 105 | with: 106 | token: ${{ secrets.CODECOV_TOKEN }} 107 | files: /home/runner/work/ngio/ngio/coverage.xml 108 | 109 | deploy: 110 | name: Deploy 111 | needs: test 112 | if: success() && startsWith(github.ref, 'refs/tags/') && github.event_name != 'schedule' 113 | runs-on: ubuntu-latest 114 | 115 | permissions: 116 | # IMPORTANT: this permission is mandatory for trusted publishing on PyPi 117 | # see https://docs.pypi.org/trusted-publishers/ 118 | id-token: write 119 | # This permission allows writing releases 120 | contents: write 121 | 122 | steps: 123 | - uses: actions/checkout@v4 124 | with: 125 | fetch-depth: 0 126 | 127 | - name: 🐍 Set up Python 128 | uses: actions/setup-python@v5 129 | with: 130 | python-version: "3.x" 131 | 132 | - name: 👷 Build 133 | run: | 134 | python -m pip install build 135 | python -m build 136 | 137 | - name: 🚢 Publish to PyPI 138 | uses: pypa/gh-action-pypi-publish@release/v1 139 | 140 | - uses: softprops/action-gh-release@v2 141 | with: 142 | generate_release_notes: true 143 | files: './dist/*' 144 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | .DS_Store 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | *.ipynb_checkpoints 75 | *.ipynb 76 | !docs/**/*.ipynb 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # celery beat schedule file 82 | celerybeat-schedule 83 | 84 | # SageMath parsed files 85 | *.sage.py 86 | 87 | # dotenv 88 | .env 89 | 90 | # virtualenv 91 | .venv 92 | venv/ 93 | ENV/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | # ruff 109 | .ruff_cache/ 110 | 111 | # IDE settings 112 | .vscode/ 113 | .idea/ 114 | # pixi environments 115 | .pixi 116 | pixi.lock 117 | *.egg-info 118 | 119 | # Ignore all .zarr directories 120 | *.zarr 121 | # but allow .zarr in tests/data 122 | !tests/data/**/**/test_*.zarr 123 | 124 | # ignore data directory 125 | ./data/ 126 | *.zip 127 | 128 | src/ngio/_v01 129 | tests/_v01 130 | 131 | # Ignore locks 132 | *.lock 133 | 134 | benchmark/* -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # enable pre-commit.ci at https://pre-commit.ci/ 2 | # it adds: 3 | # 1. auto fixing pull requests 4 | # 2. auto updating the pre-commit configuration 5 | ci: 6 | autoupdate_schedule: monthly 7 | autofix_commit_msg: "style(pre-commit.ci): auto fixes [...]" 8 | autoupdate_commit_msg: "ci(pre-commit.ci): autoupdate" 9 | 10 | repos: 11 | - repo: https://github.com/abravalheri/validate-pyproject 12 | rev: v0.18 13 | hooks: 14 | - id: validate-pyproject 15 | 16 | - repo: https://github.com/crate-ci/typos 17 | rev: typos-dict-v0.11.20 18 | hooks: 19 | - id: typos 20 | #args: [--force-exclude] # omitting --write-changes 21 | args: [--force-exclude, --write-changes] 22 | 23 | - repo: https://github.com/charliermarsh/ruff-pre-commit 24 | rev: v0.9.6 25 | hooks: 26 | - id: ruff 27 | args: [--fix] # may also add '--unsafe-fixes' 28 | - id: ruff-format 29 | 30 | #- repo: https://github.com/pre-commit/mirrors-mypy 31 | # rev: v1.10.0 32 | # hooks: 33 | # - id: mypy 34 | # files: "^src/" 35 | # # # you have to add the things you want to type check against here 36 | # # additional_dependencies: 37 | # # - numpy 38 | 39 | - repo: https://github.com/kynan/nbstripout 40 | rev: 0.7.1 41 | hooks: 42 | - id: nbstripout 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Lorenzo Cerrone 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGIO - Next Generation file format IO 2 | 3 | [![License](https://img.shields.io/pypi/l/ngio.svg?color=green)](https://github.com/lorenzocerrone/ngio/raw/main/LICENSE) 4 | [![PyPI](https://img.shields.io/pypi/v/ngio.svg?color=green)](https://pypi.org/project/ngio) 5 | [![Python Version](https://img.shields.io/pypi/pyversions/ngio.svg?color=green)](https://python.org) 6 | [![CI](https://github.com/fractal-analytics-platform/ngio/actions/workflows/ci.yml/badge.svg)](https://github.com/fractal-analytics-platform/ngio/actions/workflows/ci.yml) 7 | [![codecov](https://codecov.io/gh/fractal-analytics-platform/ngio/graph/badge.svg?token=FkmF26FZki)](https://codecov.io/gh/fractal-analytics-platform/ngio) 8 | 9 | NGIO is a Python library to streamline OME-Zarr image analysis workflows. 10 | 11 | **Main Goals:** 12 | 13 | - Abstract object base API for handling OME-Zarr files 14 | - Powerful iterators for processing data using common access patterns 15 | - Tight integration with [Fractal's Table Fractal](https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/) 16 | - Validation of OME-Zarr files 17 | 18 | To get started, check out the [Getting Started](https://fractal-analytics-platform.github.io/ngio/getting-started/) guide. Or checkout our [Documentation](https://fractal-analytics-platform.github.io/ngio/) 19 | 20 | ## 🚧 Ngio is Under active Development 🚧 21 | 22 | ### Roadmap 23 | 24 | | Feature | Status | ETA | Description | 25 | |---------|--------|-----|-------------| 26 | | Metadata Handling | ✅ | | Read, Write, Validate OME-Zarr Metadata (0.4 supported, 0.5 ready) | 27 | | OME-Zarr Validation | ✅ | | Validate OME-Zarr files for compliance with the OME-Zarr Specification + Compliance between Metadata and Data | 28 | | Base Image Handling | ✅ | | Load data from OME-Zarr files, retrieve basic metadata, and write data | 29 | | ROI Handling | ✅ | | Common ROI models | 30 | | Label Handling | ✅ | Mid-September | Based on Image Handling | 31 | | Table Validation | ✅ | Mid-September | Validate Table fractal V1 + Compliance between Metadata and Data | 32 | | Table Handling | ✅ | Mid-September | Read, Write ROI, Features, and Masked Tables | 33 | | Basic Iterators | Ongoing | End-September | Read and Write Iterators for common access patterns | 34 | | Base Documentation | ✅ | End-September | API Documentation and Examples | 35 | | Beta Ready Testing | ✅ | End-September | Beta Testing; Library is ready for testing, but the API is not stable | 36 | | Streaming from Fractal | Ongoing | December | Ngio can stream OME-Zarr from fractal | 37 | | Mask Iterators | Ongoing | Early 2025 | Iterators over Masked Tables | 38 | | Advanced Iterators | Not started | mid-2025 | Iterators for advanced access patterns | 39 | | Parallel Iterators | Not started | mid-2025 | Concurrent Iterators for parallel read and write | 40 | | Full Documentation | Not started | 2025 | Complete Documentation | 41 | | Release 1.0 (Commitment to API) | Not started | 2025 | API is stable; breaking changes will be avoided | 42 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | # Don't correct the surname "Teh" 3 | OME = "OME" 4 | FO = "FO" 5 | -------------------------------------------------------------------------------- /docs/api/common.md: -------------------------------------------------------------------------------- 1 | # ngio.common API documentation 2 | 3 | !!! warning 4 | Coming soon, ngio API documentation is not yet available. 5 | If you have any questions please reach out to us on GitHub or via email. 6 | We are happy to help you with any questions or issues you may have. 7 | -------------------------------------------------------------------------------- /docs/api/hcs.md: -------------------------------------------------------------------------------- 1 | # ngio.hcs API documentation 2 | 3 | !!! warning 4 | Coming soon, ngio API documentation is not yet available. 5 | If you have any questions please reach out to us on GitHub or via email. 6 | We are happy to help you with any questions or issues you may have. 7 | -------------------------------------------------------------------------------- /docs/api/images.md: -------------------------------------------------------------------------------- 1 | # ngio.images API documentation 2 | 3 | !!! warning 4 | Coming soon, ngio API documentation is not yet available. 5 | If you have any questions please reach out to us on GitHub or via email. 6 | We are happy to help you with any questions or issues you may have. 7 | -------------------------------------------------------------------------------- /docs/api/ngio.md: -------------------------------------------------------------------------------- 1 | # ngio API documentation 2 | 3 | !!! warning 4 | Coming soon, ngio API documentation is not yet available. 5 | If you have any questions please reach out to us on GitHub or via email. 6 | We are happy to help you with any questions or issues you may have. 7 | -------------------------------------------------------------------------------- /docs/api/tables.md: -------------------------------------------------------------------------------- 1 | # ngio.tables API documentation 2 | 3 | !!! warning 4 | Coming soon, ngio API documentation is not yet available. 5 | If you have any questions please reach out to us on GitHub or via email. 6 | We are happy to help you with any questions or issues you may have. 7 | -------------------------------------------------------------------------------- /docs/api/utils.md: -------------------------------------------------------------------------------- 1 | # ngio.utils 2 | 3 | !!! warning 4 | Coming soon, ngio API documentation is not yet available. 5 | If you have any questions please reach out to us on GitHub or via email. 6 | We are happy to help you with any questions or issues you may have. 7 | -------------------------------------------------------------------------------- /docs/code_of_conduct.md: -------------------------------------------------------------------------------- 1 | !!! warning 2 | The library is still in the early stages of development, the code of conduct is not yet established. 3 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | !!! warning 2 | The library is still in the early stages of development, no contribution guidelines are established yet. 3 | But contributions are welcome! Please open an issue or a pull request to discuss your ideas. 4 | We are looking for contributors to help us improve the library and documentation. 5 | -------------------------------------------------------------------------------- /docs/getting_started/0_quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | Ngio is a Python package that provides a simple and intuitive API for reading and writing data to and from OME-Zarr. This guide will walk you through the basics of using `ngio` to read and write data. 4 | 5 | ## Installation 6 | 7 | `ngio` can be installed from PyPI, conda-forge, or from source. 8 | 9 | - `ngio` requires Python `>=3.11` 10 | 11 | === "pip" 12 | 13 | The recommended way to install `ngio` is from PyPI using pip: 14 | 15 | ```bash 16 | pip install ngio 17 | ``` 18 | 19 | === "mamba/conda" 20 | 21 | Alternatively, you can install `ngio` using mamba: 22 | 23 | ```bash 24 | mamba install -c conda-forge ngio 25 | ``` 26 | 27 | or conda: 28 | 29 | ```bash 30 | conda install -c conda-forge ngio 31 | ``` 32 | 33 | === "Source" 34 | 35 | 1. Clone the repository: 36 | ```bash 37 | git clone https://github.com/fractal-analytics-platform/ngio.git 38 | cd ngio 39 | ``` 40 | 41 | 2. Install the package: 42 | ```bash 43 | pip install . 44 | ``` 45 | 46 | ### Troubleshooting 47 | 48 | Please report installation problems by opening an issue on our [GitHub repository](https://github.com/fractal-analytics-platform/ngio). 49 | 50 | ## Setup some test data 51 | 52 | Let's start by downloading a sample OME-Zarr dataset to work with. 53 | 54 | ```python exec="true" source="material-block" session="quickstart" 55 | from pathlib import Path 56 | from ngio.utils import download_ome_zarr_dataset 57 | 58 | # Download a sample dataset 59 | download_dir = Path("./data") 60 | download_dir = Path(".").absolute().parent.parent / "data" # markdown-exec: hide 61 | hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) 62 | image_path = hcs_path / "B" / "03" / "0" 63 | ``` 64 | 65 | ## Open an OME-Zarr image 66 | 67 | Let's start by opening an OME-Zarr file and inspecting its contents. 68 | 69 | ```pycon exec="true" source="console" session="quickstart" 70 | >>> from ngio import open_ome_zarr_container 71 | >>> ome_zarr_container = open_ome_zarr_container(image_path) 72 | >>> ome_zarr_container 73 | >>> print(ome_zarr_container) # markdown-exec: hide 74 | ``` 75 | 76 | ### What is the OME-Zarr container? 77 | 78 | The `OME-Zarr Container` is the core of ngio and the entry point to working with OME-Zarr images. It provides high-level access to the image metadata, images, labels, and tables. 79 | 80 | ### What is the OME-Zarr container not? 81 | 82 | The `OME-Zarr Container` object does not allow the user to interact with the image data directly. For that, we need to use the `Image`, `Label`, and `Table` objects. 83 | 84 | ## Next steps 85 | 86 | To learn how to work with the `OME-Zarr Container` object, but also with the image, label, and table data, check out the following guides: 87 | 88 | - [OME-Zarr Container](1_ome_zarr_containers.md): An overview on how to use the OME-Zarr Container object and how to create new images and labels. 89 | - [Images/Labels](2_images.md): To know more on how to work with image data. 90 | - [Tables](3_tables.md): To know more on how to work with table data, and how you can combine tables with image data. 91 | - [Masked Images/Labels](4_masked_images.md): To know more on how to work with masked image data. 92 | - [HCS Plates](5_hcs.md): To know more on how to work with HCS plate data. 93 | 94 | Also, checkout our jupyer notebook tutorials for more examples: 95 | 96 | - [Image Processing](../tutorials/image_processing.ipynb): Learn how to perform simple image processing operations. 97 | - [Image Segmentation](../tutorials/image_segmentation.ipynb): Learn how to create new labels from images. 98 | - [Feature Extraction](../tutorials/feature_extraction.ipynb): Learn how to extract features from images. 99 | - [HCS Processing](../tutorials/hcs_processing.ipynb): Learn how to process high-content screening data using ngio. 100 | -------------------------------------------------------------------------------- /docs/getting_started/4_masked_images.md: -------------------------------------------------------------------------------- 1 | # 4. Masked Images and Labels 2 | 3 | Masked images (or labels) are images that are masked by an instance segmentation mask. 4 | 5 | In this section we will show how to create a `MaskedImage` object and how to use it to get the data of the image. 6 | 7 | ```python exec="true" session="masked_images" 8 | from pathlib import Path 9 | from ngio import open_ome_zarr_container 10 | from ngio.utils import download_ome_zarr_dataset 11 | 12 | # Download a sample dataset 13 | download_dir = Path(".").absolute().parent.parent / "data" 14 | hcs_path = download_ome_zarr_dataset("CardiomyocyteSmallMip", download_dir=download_dir) 15 | image_path = hcs_path / "B" / "03" / "0" 16 | 17 | # Open the OME-Zarr container 18 | ome_zarr_container = open_ome_zarr_container(image_path) 19 | ``` 20 | 21 | Similar to the `Image` and `Label` objects, the `MaskedImage` can be initialized from an `OME-Zarr Container` object using the `get_masked_image` method. 22 | 23 | Let's create a masked image from the `nuclei` label: 24 | 25 | ```pycon exec="true" source="console" session="masked_images" 26 | >>> masked_image = ome_zarr_container.get_masked_image("nuclei") 27 | >>> masked_image 28 | >>> print(masked_image) # markdown-exec: hide 29 | ``` 30 | 31 | Since the `MaskedImage` is a subclass of `Image`, we can use all the methods available for `Image` objects. 32 | 33 | The two most notable exceptions are the `get_roi` and `set_roi` which now instead of requiring a `roi` object, require an integer `label`. 34 | 35 | ```pycon exec="true" source="console" session="masked_images" 36 | >>> roi_data = masked_image.get_roi(label=1009, c=0) 37 | >>> roi_data.shape 38 | >>> print(roi_data.shape) # markdown-exec: hide 39 | ``` 40 | 41 | ```python exec="1" html="1" session="masked_images" 42 | from io import StringIO 43 | import matplotlib.pyplot as plt 44 | import numpy as np 45 | # Create a random colormap for labels 46 | from matplotlib.colors import ListedColormap 47 | from matplotlib.patches import Rectangle 48 | np.random.seed(0) 49 | cmap_array = np.random.rand(1000, 3) 50 | cmap_array[0] = 0 51 | cmap = ListedColormap(cmap_array) 52 | 53 | image_data = masked_image.get_roi(label=1009, c=0) 54 | image_data = np.squeeze(image_data) 55 | 56 | fig, ax = plt.subplots(figsize=(8, 4)) 57 | ax.set_title("Label 1009 ROI") 58 | ax.imshow(image_data, cmap='gray') 59 | 60 | ax.axis('off') 61 | fig.tight_layout() 62 | buffer = StringIO() 63 | plt.savefig(buffer, format="svg") 64 | print(buffer.getvalue()) 65 | ``` 66 | 67 | Additionally we can used the `zoom_factor` argument to get more context around the ROI. 68 | For example we can zoom out the ROI by a factor of `2`: 69 | 70 | ```pycon exec="true" source="console" session="masked_images" 71 | >>> roi_data = masked_image.get_roi(label=1009, c=0, zoom_factor=2) 72 | >>> roi_data.shape 73 | >>> print(roi_data.shape) # markdown-exec: hide 74 | ``` 75 | 76 | ```python exec="1" html="1" session="masked_images" 77 | image_data = masked_image.get_roi(label=1009, c=0, zoom_factor=2) 78 | image_data = np.squeeze(image_data) 79 | 80 | fig, ax = plt.subplots(figsize=(8, 4)) 81 | ax.set_title("Label 1009 ROI - Zoomed out") 82 | ax.imshow(image_data, cmap='gray') 83 | 84 | ax.axis('off') 85 | fig.tight_layout() 86 | buffer = StringIO() 87 | plt.savefig(buffer, format="svg") 88 | print(buffer.getvalue()) 89 | ``` 90 | 91 | ## Masked operations 92 | 93 | In addition to the `get_roi` method, the `MaskedImage` class also provides a masked operation method that allows you to perform reading and writing only on the masked pixels. 94 | 95 | For these operations we can use the `get_roi_masked` and `set_roi_masked` methods. 96 | For example, we can use the `get_roi_masked` method to get the masked data for a specific label: 97 | 98 | ```pycon exec="true" source="console" session="masked_images" 99 | >>> masked_roi_data = masked_image.get_roi_masked(label=1009, c=0, zoom_factor=2) 100 | >>> masked_roi_data.shape 101 | >>> print(masked_roi_data.shape) # markdown-exec: hide 102 | ``` 103 | 104 | ```python exec="1" html="1" session="masked_images" 105 | masked_roi_data = masked_image.get_roi_masked(label=1009, c=0, zoom_factor=2) 106 | masked_roi_data = np.squeeze(masked_roi_data) 107 | fig, ax = plt.subplots(figsize=(8, 4)) 108 | ax.set_title("Masked Label 1009 ROI") 109 | ax.imshow(masked_roi_data, cmap='gray') 110 | ax.axis('off') 111 | fig.tight_layout() 112 | buffer = StringIO() 113 | plt.savefig(buffer, format="svg") 114 | print(buffer.getvalue()) 115 | ``` 116 | 117 | We can also use the `set_roi_masked` method to set the masked data for a specific label: 118 | 119 | ```pycon exec="true" source="console" session="masked_images" 120 | >>> masked_data = masked_image.get_roi_masked(label=1009, c=0) 121 | >>> masked_data = np.random.randint(0, 255, masked_data.shape, dtype=np.uint8) 122 | >>> masked_image.set_roi_masked(label=1009, c=0, patch=masked_data) 123 | ``` 124 | 125 | ```python exec="1" html="1" session="masked_images" 126 | masked_data = masked_image.get_roi(label=1009, c=0, zoom_factor=2) 127 | masked_data = np.squeeze(masked_data) 128 | fig, ax = plt.subplots(figsize=(8, 4)) 129 | ax.set_title("Masked Label 1009 ROI - After setting") 130 | ax.imshow(masked_data, cmap='gray') 131 | ax.axis('off') 132 | fig.tight_layout() 133 | buffer = StringIO() 134 | plt.savefig(buffer, format="svg") 135 | print(buffer.getvalue()) 136 | ``` 137 | 138 | ## Masked Labels 139 | 140 | The `MaskedLabel` class is a subclass of `Label` and provides the same functionality as the `MaskedImage` class. 141 | 142 | The `MaskedLabel` class can be used to create a masked label from an `OME-Zarr Container` object using the `get_masked_label` method. 143 | 144 | ```pycon exec="true" source="console" session="masked_images" 145 | >>> masked_label = ome_zarr_container.get_masked_label(label_name = "wf_2_labels", masking_label_name = "nuclei") 146 | >>> masked_label 147 | >>> print(masked_label) # markdown-exec: hide 148 | ``` 149 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | ngio is a Python library designed to simplify bioimage analysis workflows, offering an intuitive interface for working with OME-Zarr files. 3 | 4 | ## What is Ngio? 5 | 6 | Ngio is built for the [OME-Zarr](https://ngff.openmicroscopy.org/) file format, a modern, cloud-optimized format for biological imaging data. OME-Zarr stores large, multi-dimensional microscopy images and metadata in an efficient and scalable way. 7 | 8 | Ngio's mission is to streamline working with OME-Zarr files by providing a simple, object-based API for opening, exploring, and manipulating OME-Zarr images and high-content screening (HCS) plates. It also offers comprehensive support for labels, tables and regions of interest (ROIs), making it easy to extract and analyze specific regions in your data. 9 | 10 | ## Key Features 11 | 12 | ### 📊 Simple Object-Based API 13 | 14 | - Easily open, explore, and manipulate OME-Zarr images and HCS plates 15 | - Create and derive new images and labels with minimal boilerplate code 16 | 17 | ### 🔍 Rich Tables and Regions of Interest (ROI) Support 18 | 19 | - Extract and analyze specific regions of interest 20 | - Tight integration with [Fractal's table framework](https://fractal-analytics-platform.github.io/fractal-tasks-core/tables/) 21 | 22 | ### 🔄 Scalable Data Processing (Coming Soon) 23 | 24 | - Powerful iterators for processing data at scale 25 | - Efficient memory management for large datasets 26 | 27 | ## Getting Started 28 | 29 | Refer to the [Getting Started](getting_started/0_quickstart.md) guide to integrate ngio into your workflows. We also provide a collection of [Tutorials](tutorials/image_processing.ipynb) to help you get up and running quickly. 30 | For more advanced usage and API documentation, see our [API Reference](api/ngio.md). 31 | 32 | ## Supported OME-Zarr versions 33 | Currently, ngio only supports OME-Zarr v0.4. Support for version 0.5 and higher is planned for future releases. 34 | 35 | ## Development Status 36 | 37 | !!! warning 38 | Ngio is under active development and is not yet stable. The API is subject to change, and bugs and breaking changes are expected. 39 | 40 | ### Available Features 41 | 42 | - ✅ OME-Zarr metadata handling and validation 43 | - ✅ Image and label access across pyramid levels 44 | - ✅ ROI and table support 45 | - ✅ Streaming from remote sources 46 | - ✅ Documentation and examples 47 | 48 | ### Upcoming Features 49 | 50 | - Advanced image processing iterators 51 | - Parallel processing capabilities 52 | - Support for OME-Zarr v0.5 and Zarr v3 53 | 54 | ## Contributors 55 | 56 | Ngio is developed at the [BioVisionCenter](https://www.biovisioncenter.uzh.ch/en.html), University of Zurich, by [@lorenzocerrone](https://github.com/lorenzocerrone) and [@jluethi](https://github.com/jluethi). 57 | 58 | ## License 59 | 60 | Ngio is released under the BSD-3-Clause License. See [LICENSE](https://github.com/fractal-analytics-platform/ngio/blob/main/LICENSE) for details. 61 | 62 | ## Repository 63 | 64 | Visit our [GitHub repository](https://github.com/fractal-analytics-platform/ngio) for the latest code, issues, and contributions. 65 | -------------------------------------------------------------------------------- /docs/tutorials/feature_extraction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Feature Extraction\n", 8 | "\n", 9 | "Coming soon! This section is still under construction.\n", 10 | "Please check back later for updates." 11 | ] 12 | } 13 | ], 14 | "metadata": { 15 | "kernelspec": { 16 | "display_name": "dev", 17 | "language": "python", 18 | "name": "python3" 19 | }, 20 | "language_info": { 21 | "codemirror_mode": { 22 | "name": "ipython", 23 | "version": 3 24 | }, 25 | "file_extension": ".py", 26 | "mimetype": "text/x-python", 27 | "name": "python", 28 | "nbconvert_exporter": "python", 29 | "pygments_lexer": "ipython3", 30 | "version": "3.11.11" 31 | } 32 | }, 33 | "nbformat": 4, 34 | "nbformat_minor": 2 35 | } 36 | -------------------------------------------------------------------------------- /docs/tutorials/hcs_processing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# HCS Processing\n", 8 | "\n", 9 | "Coming soon! This section is still under construction.\n", 10 | "Please check back later for updates." 11 | ] 12 | } 13 | ], 14 | "metadata": { 15 | "language_info": { 16 | "name": "python" 17 | } 18 | }, 19 | "nbformat": 4, 20 | "nbformat_minor": 2 21 | } 22 | -------------------------------------------------------------------------------- /docs/tutorials/image_processing.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Image Processing\n", 8 | "\n", 9 | "Coming soon! This section is still under construction.\n", 10 | "Please check back later for updates." 11 | ] 12 | } 13 | ], 14 | "metadata": { 15 | "language_info": { 16 | "name": "python" 17 | } 18 | }, 19 | "nbformat": 4, 20 | "nbformat_minor": 2 21 | } 22 | -------------------------------------------------------------------------------- /docs/tutorials/image_segmentation.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Image Semgmentation\n", 8 | "\n", 9 | "This is a minimal tutorial on how to use ngio for image segmentation.\n", 10 | "\n", 11 | "## Step 1: Setup\n", 12 | "\n", 13 | "We will first implement a very simple function to segment an image. We will use skimage to do this. \n" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "# Setup a simple segmentation function\n", 23 | "import numpy as np\n", 24 | "import skimage\n", 25 | "\n", 26 | "\n", 27 | "def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray:\n", 28 | " \"\"\"Simple segmentation using Otsu thresholding.\"\"\"\n", 29 | " threshold = skimage.filters.threshold_otsu(image)\n", 30 | " binary = image > threshold\n", 31 | " label_image = skimage.measure.label(binary)\n", 32 | " label_image += max_label\n", 33 | " label_image = np.where(binary, label_image, 0)\n", 34 | " return label_image" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": {}, 40 | "source": [ 41 | "## Step 2: Open the OmeZarr container" 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": null, 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "from pathlib import Path\n", 51 | "\n", 52 | "from ngio import open_ome_zarr_container\n", 53 | "from ngio.utils import download_ome_zarr_dataset\n", 54 | "\n", 55 | "# Download the dataset\n", 56 | "download_dir = Path(\".\").absolute().parent.parent / \"data\"\n", 57 | "hcs_path = download_ome_zarr_dataset(\"CardiomyocyteTiny\", download_dir=download_dir)\n", 58 | "image_path = hcs_path / \"B\" / \"03\" / \"0\"\n", 59 | "\n", 60 | "# Open the ome-zarr container\n", 61 | "ome_zarr = open_ome_zarr_container(image_path)" 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "metadata": {}, 67 | "source": [ 68 | "## Step 3: Segment the image\n", 69 | "\n", 70 | "For this example, we will not segment the image all at once. Instead we will iterate over the image FOVs and segment them one by one." 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": null, 76 | "metadata": {}, 77 | "outputs": [], 78 | "source": [ 79 | "# First we will need the image object and the FOVs table\n", 80 | "image = ome_zarr.get_image()\n", 81 | "roi_table = ome_zarr.get_table(\"FOV_ROI_table\", check_type=\"roi_table\")\n", 82 | "\n", 83 | "# Second we need to derive a new label image to use as target for the segmentation\n", 84 | "\n", 85 | "label = ome_zarr.derive_label(\"new_label\", overwrite=True)\n", 86 | "\n", 87 | "max_label = 0 # We will use this to avoid label collisions\n", 88 | "for roi in roi_table.rois():\n", 89 | " image_data = image.get_roi(roi=roi, c=0) # Get the image data for the ROI\n", 90 | "\n", 91 | " image_data = image_data.squeeze() # Remove the channel dimension\n", 92 | " roi_segmentation = otsu_threshold_segmentation(\n", 93 | " image_data, max_label\n", 94 | " ) # Segment the image\n", 95 | "\n", 96 | " max_label = roi_segmentation.max() # Get the max label for the next iteration\n", 97 | "\n", 98 | " label.set_roi(\n", 99 | " roi=roi, patch=roi_segmentation\n", 100 | " ) # Write the segmentation to the label image" 101 | ] 102 | }, 103 | { 104 | "cell_type": "markdown", 105 | "metadata": {}, 106 | "source": [ 107 | "# Step 4: Consolidate the segmentation\n", 108 | "\n", 109 | "The `new_label` has data only at a single resolution lebel. To consolidate the segmentation to all other levels we will \n", 110 | "need to call the `consolidate` method." 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": {}, 117 | "outputs": [], 118 | "source": [ 119 | "label.consolidate()" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "## Plot the segmentation" 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": null, 132 | "metadata": {}, 133 | "outputs": [], 134 | "source": [ 135 | "import matplotlib.pyplot as plt\n", 136 | "import numpy as np\n", 137 | "from matplotlib.colors import ListedColormap\n", 138 | "\n", 139 | "rand_cmap = np.random.rand(1000, 3)\n", 140 | "rand_cmap[0] = 0\n", 141 | "rand_cmap = ListedColormap(rand_cmap)\n", 142 | "\n", 143 | "fig, axs = plt.subplots(2, 1, figsize=(8, 4))\n", 144 | "axs[0].set_title(\"Original image\")\n", 145 | "axs[0].imshow(image.get_array(c=0, z=1).squeeze(), cmap=\"gray\")\n", 146 | "axs[1].set_title(\"Final segmentation\")\n", 147 | "axs[1].imshow(label.get_array(z=1).squeeze(), cmap=rand_cmap)\n", 148 | "for ax in axs:\n", 149 | " ax.axis(\"off\")\n", 150 | "plt.tight_layout()\n", 151 | "plt.show()" 152 | ] 153 | } 154 | ], 155 | "metadata": { 156 | "kernelspec": { 157 | "display_name": "dev", 158 | "language": "python", 159 | "name": "python3" 160 | }, 161 | "language_info": { 162 | "codemirror_mode": { 163 | "name": "ipython", 164 | "version": 3 165 | }, 166 | "file_extension": ".py", 167 | "mimetype": "text/x-python", 168 | "name": "python", 169 | "nbconvert_exporter": "python", 170 | "pygments_lexer": "ipython3", 171 | "version": "3.11.11" 172 | } 173 | }, 174 | "nbformat": 4, 175 | "nbformat_minor": 2 176 | } 177 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: "NGIO: Next Generation File Format I/O" 2 | site_url: "https://github.com/fractal-analytics-platform/ngio.git" 3 | site_description: "A Python library for processing OME-Zarr images" 4 | repo_name: "ngio" 5 | repo_url: "https://github.com/fractal-analytics-platform/ngio" 6 | copyright: "Copyright © 2024-, BioVisionCenter UZH" 7 | 8 | theme: 9 | name: material 10 | favicon: images/favicon.ico 11 | #logo: logos/logo_white.png 12 | icon: 13 | repo: fontawesome/brands/github 14 | palette: 15 | # Palette toggle for light mode 16 | - scheme: default 17 | # primary: green 18 | toggle: 19 | icon: material/brightness-7 20 | name: Switch to dark mode 21 | 22 | # Palette toggle for dark mode 23 | - scheme: slate 24 | # primary: teal 25 | # accent: light-green 26 | toggle: 27 | icon: material/brightness-4 28 | name: Switch to light mode 29 | features: 30 | - content.tooltips 31 | - content.tabs.link 32 | - content.code.annotate 33 | - navigation.instant 34 | - navigation.instant.progress 35 | - navigation.sections 36 | - navigation.path 37 | - navigation.indexes 38 | - navigation.footer 39 | - toc.follow 40 | - search.suggest 41 | - search.share 42 | 43 | extra: 44 | version: 45 | provider: mike 46 | social: 47 | - icon: fontawesome/brands/github 48 | link: "https://github.com/fractal-analytics-platform/ngio" 49 | name: NGIO on GitHub 50 | 51 | plugins: 52 | - search 53 | - autorefs 54 | - markdown-exec 55 | - mkdocstrings: 56 | handlers: 57 | python: 58 | import: 59 | - https://docs.python.org/3/objects.inv 60 | - https://numpy.org/doc/stable/objects.inv 61 | options: 62 | heading_level: 3 63 | docstring_style: google 64 | show_source: true 65 | show_signature_annotations: true 66 | show_root_heading: true 67 | show_root_full_path: true 68 | show_bases: true 69 | docstring_section_style: list 70 | - git-revision-date-localized: 71 | enable_creation_date: true 72 | - git-committers: 73 | repository: fractal-analytics-platform/ngio 74 | branch: main 75 | - mkdocs-jupyter: 76 | execute: true 77 | 78 | markdown_extensions: 79 | - admonition 80 | - pymdownx.details 81 | - pymdownx.superfences 82 | - md_in_html 83 | - pymdownx.tabbed: 84 | alternate_style: true 85 | 86 | nav: 87 | - "NGIO: Streamlined OME-Zarr Image Analysis": index.md 88 | - Getting Started: 89 | - getting_started/0_quickstart.md 90 | - getting_started/1_ome_zarr_containers.md 91 | - getting_started/2_images.md 92 | - getting_started/3_tables.md 93 | - getting_started/4_masked_images.md 94 | - getting_started/5_hcs.md 95 | 96 | - Tutorials: 97 | - tutorials/image_processing.ipynb 98 | - tutorials/image_segmentation.ipynb 99 | - tutorials/feature_extraction.ipynb 100 | - tutorials/hcs_processing.ipynb 101 | 102 | - API Reference: 103 | - "ngio": api/ngio.md 104 | - "ngio.images": api/images.md 105 | - "ngio.tables": api/tables.md 106 | - "ngio.hcs": api/hcs.md 107 | - "ngio.utils": api/utils.md 108 | - "ngio.common": api/common.md 109 | - Contributing: 110 | - "Contributing Guide": contributing.md 111 | - "Code of Conduct": code_of_conduct.md -------------------------------------------------------------------------------- /src/ngio/__init__.py: -------------------------------------------------------------------------------- 1 | """Next Generation file format IO.""" 2 | 3 | from importlib.metadata import PackageNotFoundError, version 4 | 5 | try: 6 | __version__ = version("ngio") 7 | except PackageNotFoundError: # pragma: no cover 8 | __version__ = "uninstalled" 9 | __author__ = "Lorenzo Cerrone" 10 | __email__ = "lorenzo.cerrone@uzh.ch" 11 | 12 | from ngio.common import ArrayLike, Dimensions, Roi, RoiPixels 13 | from ngio.hcs import ( 14 | OmeZarrPlate, 15 | OmeZarrWell, 16 | create_empty_plate, 17 | create_empty_well, 18 | open_ome_zarr_plate, 19 | open_ome_zarr_well, 20 | ) 21 | from ngio.images import ( 22 | Image, 23 | Label, 24 | OmeZarrContainer, 25 | create_empty_ome_zarr, 26 | create_ome_zarr_from_array, 27 | open_image, 28 | open_ome_zarr_container, 29 | ) 30 | from ngio.ome_zarr_meta.ngio_specs import ( 31 | AxesSetup, 32 | DefaultNgffVersion, 33 | ImageInWellPath, 34 | NgffVersions, 35 | PixelSize, 36 | ) 37 | 38 | __all__ = [ 39 | "ArrayLike", 40 | "AxesSetup", 41 | "DefaultNgffVersion", 42 | "Dimensions", 43 | "Image", 44 | "ImageInWellPath", 45 | "Label", 46 | "NgffVersions", 47 | "OmeZarrContainer", 48 | "OmeZarrPlate", 49 | "OmeZarrWell", 50 | "PixelSize", 51 | "Roi", 52 | "RoiPixels", 53 | "create_empty_ome_zarr", 54 | "create_empty_plate", 55 | "create_empty_well", 56 | "create_ome_zarr_from_array", 57 | "open_image", 58 | "open_ome_zarr_container", 59 | "open_ome_zarr_plate", 60 | "open_ome_zarr_well", 61 | ] 62 | -------------------------------------------------------------------------------- /src/ngio/common/__init__.py: -------------------------------------------------------------------------------- 1 | """Common classes and functions that are used across the package.""" 2 | 3 | from ngio.common._array_pipe import ( 4 | get_masked_pipe, 5 | get_pipe, 6 | set_masked_pipe, 7 | set_pipe, 8 | ) 9 | from ngio.common._axes_transforms import ( 10 | transform_dask_array, 11 | transform_list, 12 | transform_numpy_array, 13 | ) 14 | from ngio.common._common_types import ArrayLike 15 | from ngio.common._dimensions import Dimensions 16 | from ngio.common._masking_roi import compute_masking_roi 17 | from ngio.common._pyramid import consolidate_pyramid, init_empty_pyramid, on_disk_zoom 18 | from ngio.common._roi import Roi, RoiPixels, roi_to_slice_kwargs 19 | from ngio.common._slicer import ( 20 | SliceTransform, 21 | compute_and_slices, 22 | dask_get_slice, 23 | dask_set_slice, 24 | numpy_get_slice, 25 | numpy_set_slice, 26 | ) 27 | from ngio.common._zoom import dask_zoom, numpy_zoom 28 | 29 | __all__ = [ 30 | "ArrayLike", 31 | "Dimensions", 32 | "Roi", 33 | "RoiPixels", 34 | "SliceTransform", 35 | "compute_and_slices", 36 | "compute_masking_roi", 37 | "consolidate_pyramid", 38 | "dask_get_slice", 39 | "dask_set_slice", 40 | "dask_zoom", 41 | "get_masked_pipe", 42 | "get_pipe", 43 | "init_empty_pyramid", 44 | "numpy_get_slice", 45 | "numpy_set_slice", 46 | "numpy_zoom", 47 | "on_disk_zoom", 48 | "roi_to_slice_kwargs", 49 | "set_masked_pipe", 50 | "set_pipe", 51 | "transform_dask_array", 52 | "transform_list", 53 | "transform_numpy_array", 54 | ] 55 | -------------------------------------------------------------------------------- /src/ngio/common/_axes_transforms.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar 2 | 3 | import dask.array as da 4 | import numpy as np 5 | 6 | from ngio.ome_zarr_meta.ngio_specs._axes import ( 7 | AxesExpand, 8 | AxesSqueeze, 9 | AxesTransformation, 10 | AxesTranspose, 11 | ) 12 | from ngio.utils import NgioValueError 13 | 14 | T = TypeVar("T") 15 | 16 | 17 | def transform_list( 18 | input_list: list[T], default: T, operations: tuple[AxesTransformation, ...] 19 | ) -> list[T]: 20 | if isinstance(input_list, tuple): 21 | input_list = list(input_list) 22 | 23 | for operation in operations: 24 | if isinstance(operation, AxesTranspose): 25 | input_list = [input_list[i] for i in operation.axes] 26 | 27 | if isinstance(operation, AxesExpand): 28 | for ax in operation.axes: 29 | input_list.insert(ax, default) 30 | elif isinstance(operation, AxesSqueeze): 31 | for offset, ax in enumerate(operation.axes): 32 | input_list.pop(ax - offset) 33 | 34 | return input_list 35 | 36 | 37 | def transform_numpy_array( 38 | array: np.ndarray, operations: tuple[AxesTransformation, ...] 39 | ) -> np.ndarray: 40 | for operation in operations: 41 | if isinstance(operation, AxesTranspose): 42 | array = np.transpose(array, operation.axes) 43 | elif isinstance(operation, AxesExpand): 44 | array = np.expand_dims(array, axis=operation.axes) 45 | elif isinstance(operation, AxesSqueeze): 46 | array = np.squeeze(array, axis=operation.axes) 47 | else: 48 | raise NgioValueError(f"Unknown operation {operation}") 49 | return array 50 | 51 | 52 | def transform_dask_array( 53 | array: da.Array, operations: tuple[AxesTransformation, ...] 54 | ) -> da.Array: 55 | for operation in operations: 56 | if isinstance(operation, AxesTranspose): 57 | array = da.transpose(array, axes=operation.axes) 58 | elif isinstance(operation, AxesExpand): 59 | array = da.expand_dims(array, axis=operation.axes) 60 | elif isinstance(operation, AxesSqueeze): 61 | array = da.squeeze(array, axis=operation.axes) 62 | else: 63 | raise NgioValueError(f"Unknown operation {operation}") 64 | return array 65 | -------------------------------------------------------------------------------- /src/ngio/common/_common_types.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import zarr 3 | from dask import array as da 4 | 5 | ArrayLike = np.ndarray | da.core.Array | zarr.Array # type: ignore 6 | -------------------------------------------------------------------------------- /src/ngio/common/_dimensions.py: -------------------------------------------------------------------------------- 1 | """Dimension metadata. 2 | 3 | This is not related to the NGFF metadata, 4 | but it is based on the actual metadata of the image data. 5 | """ 6 | 7 | from collections.abc import Collection 8 | 9 | from ngio.common._axes_transforms import transform_list 10 | from ngio.ome_zarr_meta import AxesMapper 11 | from ngio.utils import NgioValidationError, NgioValueError 12 | 13 | 14 | class Dimensions: 15 | """Dimension metadata.""" 16 | 17 | def __init__( 18 | self, 19 | shape: tuple[int, ...], 20 | axes_mapper: AxesMapper, 21 | ) -> None: 22 | """Create a Dimension object from a Zarr array. 23 | 24 | Args: 25 | shape: The shape of the Zarr array. 26 | axes_mapper: The axes mapper object. 27 | """ 28 | self._shape = shape 29 | self._axes_mapper = axes_mapper 30 | 31 | if len(self._shape) != len(self._axes_mapper.on_disk_axes): 32 | raise NgioValidationError( 33 | "The number of dimensions must match the number of axes. " 34 | f"Expected Axis {self._axes_mapper.on_disk_axes_names} but got shape " 35 | f"{self._shape}." 36 | ) 37 | 38 | def __str__(self) -> str: 39 | """Return the string representation of the object.""" 40 | dims = ", ".join( 41 | f"{ax.on_disk_name}: {s}" 42 | for ax, s in zip(self._axes_mapper.on_disk_axes, self._shape, strict=True) 43 | ) 44 | return f"Dimensions({dims})" 45 | 46 | def get(self, axis_name: str, strict: bool = True) -> int: 47 | """Return the dimension of the given axis name. 48 | 49 | Args: 50 | axis_name: The name of the axis (either canonical or non-canonical). 51 | strict: If True, raise an error if the axis does not exist. 52 | """ 53 | index = self._axes_mapper.get_index(axis_name) 54 | if index is None and strict: 55 | raise NgioValueError(f"Axis {axis_name} does not exist.") 56 | elif index is None: 57 | return 1 58 | return self._shape[index] 59 | 60 | def has_axis(self, axis_name: str) -> bool: 61 | """Return whether the axis exists.""" 62 | index = self._axes_mapper.get_axis(axis_name) 63 | if index is None: 64 | return False 65 | return True 66 | 67 | def get_shape(self, axes_order: Collection[str]) -> tuple[int, ...]: 68 | """Return the shape in the given axes order.""" 69 | transforms = self._axes_mapper.to_order(axes_order) 70 | return tuple(transform_list(list(self._shape), 1, transforms)) 71 | 72 | def get_canonical_shape(self) -> tuple[int, ...]: 73 | """Return the shape in the canonical order.""" 74 | transforms = self._axes_mapper.to_canonical() 75 | return tuple(transform_list(list(self._shape), 1, transforms)) 76 | 77 | def __repr__(self) -> str: 78 | """Return the string representation of the object.""" 79 | return str(self) 80 | 81 | @property 82 | def on_disk_shape(self) -> tuple[int, ...]: 83 | """Return the shape as a tuple.""" 84 | return tuple(self._shape) 85 | 86 | @property 87 | def is_time_series(self) -> bool: 88 | """Return whether the data is a time series.""" 89 | if self.get("t", strict=False) == 1: 90 | return False 91 | return True 92 | 93 | @property 94 | def is_2d(self) -> bool: 95 | """Return whether the data is 2D.""" 96 | if self.get("z", strict=False) != 1: 97 | return False 98 | return True 99 | 100 | @property 101 | def is_2d_time_series(self) -> bool: 102 | """Return whether the data is a 2D time series.""" 103 | return self.is_2d and self.is_time_series 104 | 105 | @property 106 | def is_3d(self) -> bool: 107 | """Return whether the data is 3D.""" 108 | return not self.is_2d 109 | 110 | @property 111 | def is_3d_time_series(self) -> bool: 112 | """Return whether the data is a 3D time series.""" 113 | return self.is_3d and self.is_time_series 114 | 115 | @property 116 | def is_multi_channels(self) -> bool: 117 | """Return whether the data has multiple channels.""" 118 | if self.get("c", strict=False) == 1: 119 | return False 120 | return True 121 | -------------------------------------------------------------------------------- /src/ngio/common/_masking_roi.py: -------------------------------------------------------------------------------- 1 | """Utilities to build masking regions of interest (ROIs).""" 2 | 3 | import itertools 4 | 5 | import dask 6 | import dask.array as da 7 | import dask.delayed 8 | import numpy as np 9 | import scipy.ndimage as ndi 10 | 11 | from ngio.common._roi import Roi, RoiPixels 12 | from ngio.ome_zarr_meta import PixelSize 13 | from ngio.utils import NgioValueError 14 | 15 | 16 | def _compute_offsets(chunks): 17 | """Given a chunks tuple, compute cumulative offsets for each axis. 18 | 19 | Returns a list where each element is a list of offsets for that dimension. 20 | """ 21 | offsets = [] 22 | for dim_chunks in chunks: 23 | dim_offsets = [0] 24 | for size in dim_chunks: 25 | dim_offsets.append(dim_offsets[-1] + size) 26 | offsets.append(dim_offsets) 27 | return offsets 28 | 29 | 30 | def _adjust_slices(slices, offset): 31 | """Adjust slices to global coordinates using the provided offset.""" 32 | adjusted_slices = {} 33 | for label, s in slices.items(): 34 | adjusted = tuple( 35 | slice(s_dim.start + off, s_dim.stop + off) 36 | for s_dim, off in zip(s, offset, strict=True) 37 | ) 38 | adjusted_slices[label] = adjusted 39 | return adjusted_slices 40 | 41 | 42 | @dask.delayed 43 | def _process_chunk(chunk, offset): 44 | """Process a single chunk. 45 | 46 | run ndi.find_objects and adjust the slices 47 | to global coordinates using the provided offset. 48 | """ 49 | local_slices = compute_slices(chunk) 50 | local_slices = _adjust_slices(local_slices, offset) 51 | return local_slices 52 | 53 | 54 | def _merge_slices( 55 | slice1: tuple[slice, ...], slice2: tuple[slice, ...] 56 | ) -> tuple[slice, ...]: 57 | """Merge two slices.""" 58 | merged = [] 59 | for s1, s2 in zip(slice1, slice2, strict=True): 60 | start = min(s1.start, s2.start) 61 | stop = max(s1.stop, s2.stop) 62 | merged.append(slice(start, stop)) 63 | return tuple(merged) 64 | 65 | 66 | @dask.delayed 67 | def _collect_slices( 68 | local_slices: list[dict[int, tuple[slice, ...]]], 69 | ) -> dict[int, tuple[slice]]: 70 | """Collect the slices from the delayed results.""" 71 | global_slices = {} 72 | for result in local_slices: 73 | for label, s in result.items(): 74 | if label in global_slices: 75 | global_slices[label] = _merge_slices(global_slices[label], s) 76 | else: 77 | global_slices[label] = s 78 | return global_slices 79 | 80 | 81 | def compute_slices(segmentation: np.ndarray) -> dict[int, tuple[slice, ...]]: 82 | """Compute slices for each label in a segmentation. 83 | 84 | Args: 85 | segmentation (ndarray): The segmentation array. 86 | 87 | Returns: 88 | dict[int, tuple[slice]]: A dictionary with the label as key 89 | and the slice as value. 90 | """ 91 | slices = ndi.find_objects(segmentation) 92 | slices_dict = {} 93 | for label, s in enumerate(slices, start=1): 94 | if s is None: 95 | continue 96 | else: 97 | slices_dict[label] = s 98 | return slices_dict 99 | 100 | 101 | def lazy_compute_slices(segmentation: da.Array) -> dict[int, tuple[slice, ...]]: 102 | """Compute slices for each label in a segmentation.""" 103 | global_offsets = _compute_offsets(segmentation.chunks) 104 | delayed_chunks = segmentation.to_delayed() 105 | 106 | grid_shape = tuple(len(c) for c in segmentation.chunks) 107 | 108 | grid_indices = list(itertools.product(*[range(n) for n in grid_shape])) 109 | delayed_results = [] 110 | for idx, chunk in zip(grid_indices, np.ravel(delayed_chunks), strict=True): 111 | offset = tuple(global_offsets[dim][idx[dim]] for dim in range(len(idx))) 112 | delayed_result = _process_chunk(chunk, offset) 113 | delayed_results.append(delayed_result) 114 | 115 | return _collect_slices(delayed_results).compute() 116 | 117 | 118 | def compute_masking_roi( 119 | segmentation: np.ndarray | da.Array, pixel_size: PixelSize 120 | ) -> list[Roi]: 121 | """Compute a ROIs for each label in a segmentation. 122 | 123 | This function expects a 2D or 3D segmentation array. 124 | And this function expects the axes order to be 'zyx' or 'yx'. 125 | Other axes orders are not supported. 126 | 127 | """ 128 | if segmentation.ndim not in [2, 3]: 129 | raise NgioValueError("Only 2D and 3D segmentations are supported.") 130 | 131 | if isinstance(segmentation, da.Array): 132 | slices = lazy_compute_slices(segmentation) 133 | else: 134 | slices = compute_slices(segmentation) 135 | 136 | rois = [] 137 | for label, slice_ in slices.items(): 138 | if len(slice_) == 2: 139 | min_z, min_y, min_x = 0, slice_[0].start, slice_[1].start 140 | max_z, max_y, max_x = 1, slice_[0].stop, slice_[1].stop 141 | elif len(slice_) == 3: 142 | min_z, min_y, min_x = slice_[0].start, slice_[1].start, slice_[2].start 143 | max_z, max_y, max_x = slice_[0].stop, slice_[1].stop, slice_[2].stop 144 | else: 145 | raise ValueError("Invalid slice length.") 146 | roi = RoiPixels( 147 | name=str(label), 148 | x_length=max_x - min_x, 149 | y_length=max_y - min_y, 150 | z_length=max_z - min_z, 151 | x=min_x, 152 | y=min_y, 153 | z=min_z, 154 | ) 155 | 156 | roi = roi.to_roi(pixel_size) 157 | rois.append(roi) 158 | return rois 159 | -------------------------------------------------------------------------------- /src/ngio/common/_roi.py: -------------------------------------------------------------------------------- 1 | """Region of interest (ROI) metadata. 2 | 3 | These are the interfaces bwteen the ROI tables / masking ROI tables and 4 | the ImageLikeHandler. 5 | """ 6 | 7 | from collections.abc import Iterable 8 | 9 | import numpy as np 10 | from pydantic import BaseModel, ConfigDict, Field 11 | 12 | from ngio.common._dimensions import Dimensions 13 | from ngio.ome_zarr_meta.ngio_specs import DefaultSpaceUnit, PixelSize, SpaceUnits 14 | from ngio.utils import NgioValueError 15 | 16 | 17 | def _to_raster(value: float, pixel_size: float, max_shape: int) -> int: 18 | """Convert to raster coordinates.""" 19 | round_value = int(np.round(value / pixel_size)) 20 | # Ensure the value is within the image shape boundaries 21 | return max(0, min(round_value, max_shape)) 22 | 23 | 24 | def _to_world(value: int, pixel_size: float) -> float: 25 | """Convert to world coordinates.""" 26 | return value * pixel_size 27 | 28 | 29 | class Roi(BaseModel): 30 | """Region of interest (ROI) metadata.""" 31 | 32 | name: str 33 | x_length: float 34 | y_length: float 35 | z_length: float = 1.0 36 | x: float = 0.0 37 | y: float = 0.0 38 | z: float = 0.0 39 | unit: SpaceUnits | str | None = Field(DefaultSpaceUnit, repr=False) 40 | 41 | model_config = ConfigDict(extra="allow") 42 | 43 | def to_pixel_roi( 44 | self, pixel_size: PixelSize, dimensions: Dimensions 45 | ) -> "RoiPixels": 46 | """Convert to raster coordinates.""" 47 | dim_x = dimensions.get("x") 48 | dim_y = dimensions.get("y") 49 | # Will default to 1 if z does not exist 50 | dim_z = dimensions.get("z", strict=False) 51 | 52 | return RoiPixels( 53 | name=self.name, 54 | x=_to_raster(self.x, pixel_size.x, dim_x), 55 | y=_to_raster(self.y, pixel_size.y, dim_y), 56 | z=_to_raster(self.z, pixel_size.z, dim_z), 57 | x_length=_to_raster(self.x_length, pixel_size.x, dim_x), 58 | y_length=_to_raster(self.y_length, pixel_size.y, dim_y), 59 | z_length=_to_raster(self.z_length, pixel_size.z, dim_z), 60 | ) 61 | 62 | def zoom(self, zoom_factor: float = 1) -> "Roi": 63 | """Zoom the ROI by a factor. 64 | 65 | Args: 66 | zoom_factor: The zoom factor. If the zoom factor 67 | is less than 1 the ROI will be zoomed in. 68 | If the zoom factor is greater than 1 the ROI will be zoomed out. 69 | If the zoom factor is 1 the ROI will not be changed. 70 | """ 71 | return zoom_roi(self, zoom_factor) 72 | 73 | 74 | class RoiPixels(BaseModel): 75 | """Region of interest (ROI) metadata.""" 76 | 77 | name: str 78 | x: int 79 | y: int 80 | z: int 81 | x_length: int 82 | y_length: int 83 | z_length: int 84 | model_config = ConfigDict(extra="allow") 85 | 86 | def to_roi(self, pixel_size: PixelSize) -> Roi: 87 | """Convert to world coordinates.""" 88 | return Roi( 89 | name=self.name, 90 | x=_to_world(self.x, pixel_size.x), 91 | y=_to_world(self.y, pixel_size.y), 92 | z=_to_world(self.z, pixel_size.z), 93 | x_length=_to_world(self.x_length, pixel_size.x), 94 | y_length=_to_world(self.y_length, pixel_size.y), 95 | z_length=_to_world(self.z_length, pixel_size.z), 96 | unit=pixel_size.space_unit, 97 | ) 98 | 99 | def to_slices(self) -> dict[str, slice]: 100 | """Return the slices for the ROI.""" 101 | return { 102 | "x": slice(self.x, self.x + self.x_length), 103 | "y": slice(self.y, self.y + self.y_length), 104 | "z": slice(self.z, self.z + self.z_length), 105 | } 106 | 107 | 108 | def zoom_roi(roi: Roi, zoom_factor: float = 1) -> Roi: 109 | """Zoom the ROI by a factor. 110 | 111 | Args: 112 | roi: The ROI to zoom. 113 | zoom_factor: The zoom factor. If the zoom factor 114 | is less than 1 the ROI will be zoomed in. 115 | If the zoom factor is greater than 1 the ROI will be zoomed out. 116 | If the zoom factor is 1 the ROI will not be changed. 117 | """ 118 | if zoom_factor <= 0: 119 | raise ValueError("Zoom factor must be greater than 0.") 120 | 121 | # the zoom factor needs to be rescaled 122 | # from the range [-1, inf) to [0, inf) 123 | zoom_factor -= 1 124 | diff_x = roi.x_length * zoom_factor 125 | diff_y = roi.y_length * zoom_factor 126 | 127 | new_x = max(roi.x - diff_x / 2, 0) 128 | new_y = max(roi.y - diff_y / 2, 0) 129 | 130 | new_roi = Roi( 131 | name=roi.name, 132 | x=new_x, 133 | y=new_y, 134 | z=roi.z, 135 | x_length=roi.x_length + diff_x, 136 | y_length=roi.y_length + diff_y, 137 | z_length=roi.z_length, 138 | unit=roi.unit, 139 | ) 140 | 141 | return new_roi 142 | 143 | 144 | def roi_to_slice_kwargs( 145 | roi: Roi, 146 | pixel_size: PixelSize, 147 | dimensions: Dimensions, 148 | **slice_kwargs: slice | int | Iterable[int], 149 | ) -> dict[str, slice | int | Iterable[int]]: 150 | """Convert a WorldCooROI to slice_kwargs.""" 151 | raster_roi = roi.to_pixel_roi( 152 | pixel_size=pixel_size, dimensions=dimensions 153 | ).to_slices() 154 | 155 | if not dimensions.has_axis(axis_name="z"): 156 | raster_roi.pop("z") 157 | 158 | for key in slice_kwargs.keys(): 159 | if key in raster_roi: 160 | raise NgioValueError( 161 | f"Key {key} is already in the slice_kwargs. " 162 | "Ambiguous which one to use: " 163 | f"{key}={slice_kwargs[key]} or roi_{key}={raster_roi[key]}" 164 | ) 165 | return {**raster_roi, **slice_kwargs} 166 | -------------------------------------------------------------------------------- /src/ngio/common/_slicer.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | 3 | import dask.array as da 4 | import numpy as np 5 | import zarr 6 | 7 | from ngio.common._dimensions import Dimensions 8 | from ngio.ome_zarr_meta.ngio_specs import AxesTransformation 9 | from ngio.utils import NgioValueError 10 | 11 | 12 | def _validate_int(value: int, shape: int) -> int: 13 | if not isinstance(value, int): 14 | raise NgioValueError(f"Invalid value {value} of type {type(value)}") 15 | if value < 0 or value >= shape: 16 | raise NgioValueError( 17 | f"Invalid value {value}. Index out of bounds for axis of shape {shape}" 18 | ) 19 | return value 20 | 21 | 22 | def _validate_iter_of_ints(value: Iterable[int], shape: int) -> list[int]: 23 | if not isinstance(value, list): 24 | raise NgioValueError(f"Invalid value {value} of type {type(value)}") 25 | value = [_validate_int(v, shape=shape) for v in value] 26 | return value 27 | 28 | 29 | def _validate_slice(value: slice, shape: int) -> slice: 30 | start = value.start if value.start is not None else 0 31 | start = max(start, 0) 32 | stop = value.stop if value.stop is not None else shape 33 | return slice(start, stop) 34 | 35 | 36 | class SliceTransform(AxesTransformation): 37 | slices: tuple[slice | tuple[int, ...], ...] 38 | 39 | 40 | def compute_and_slices( 41 | *, 42 | dimensions: Dimensions, 43 | **slice_kwargs: slice | int | Iterable[int], 44 | ) -> SliceTransform: 45 | _slices = {} 46 | axes_names = dimensions._axes_mapper.on_disk_axes_names 47 | for axis_name, slice_ in slice_kwargs.items(): 48 | axis = dimensions._axes_mapper.get_axis(axis_name) 49 | if axis is None: 50 | raise NgioValueError( 51 | f"Invalid axis {axis_name}. " 52 | f"Not found on the on-disk axes {axes_names}. " 53 | "If you want to get/set a singletorn value include " 54 | "it in the axes_order parameter." 55 | ) 56 | 57 | shape = dimensions.get(axis.on_disk_name) 58 | 59 | if isinstance(slice_, int): 60 | slice_ = _validate_int(slice_, shape) 61 | slice_ = slice(slice_, slice_ + 1) 62 | 63 | elif isinstance(slice_, Iterable): 64 | slice_ = _validate_iter_of_ints(slice_, shape) 65 | slice_ = tuple(slice_) 66 | 67 | elif isinstance(slice_, slice): 68 | slice_ = _validate_slice(slice_, shape) 69 | 70 | elif not isinstance(slice_, slice): 71 | raise NgioValueError( 72 | f"Invalid slice definition {slice_} of type {type(slice_)}" 73 | ) 74 | _slices[axis.on_disk_name] = slice_ 75 | 76 | slices = tuple(_slices.get(axis, slice(None)) for axis in axes_names) 77 | return SliceTransform(slices=slices) 78 | 79 | 80 | def numpy_get_slice(array: zarr.Array, slices: SliceTransform) -> np.ndarray: 81 | return array[slices.slices] 82 | 83 | 84 | def dask_get_slice(array: zarr.Array, slices: SliceTransform) -> da.Array: 85 | da_array = da.from_zarr(array) 86 | return da_array[slices.slices] 87 | 88 | 89 | def numpy_set_slice( 90 | array: zarr.Array, patch: np.ndarray, slices: SliceTransform 91 | ) -> None: 92 | array[slices.slices] = patch 93 | 94 | 95 | def dask_set_slice(array: zarr.Array, patch: da.Array, slices: SliceTransform) -> None: 96 | da.to_zarr(arr=patch, url=array, region=slices.slices) 97 | -------------------------------------------------------------------------------- /src/ngio/common/_zoom.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from typing import Literal 3 | 4 | import dask.array as da 5 | import numpy as np 6 | from scipy.ndimage import zoom as scipy_zoom 7 | 8 | from ngio.utils import NgioValueError 9 | 10 | 11 | def _stacked_zoom(x, zoom_y, zoom_x, order=1, mode="grid-constant", grid_mode=True): 12 | *rest, yshape, xshape = x.shape 13 | x = x.reshape(-1, yshape, xshape) 14 | scale_xy = (zoom_y, zoom_x) 15 | x_out = np.stack( 16 | [ 17 | scipy_zoom(x[i], scale_xy, order=order, mode=mode, grid_mode=True) 18 | for i in range(x.shape[0]) 19 | ] 20 | ) 21 | return x_out.reshape(*rest, *x_out.shape[1:]) 22 | 23 | 24 | def fast_zoom(x, zoom, order=1, mode="grid-constant", grid_mode=True, auto_stack=True): 25 | """Fast zoom function. 26 | 27 | Scipy zoom function that can handle singleton dimensions 28 | but the performance degrades with the number of dimensions. 29 | 30 | This function has two small optimizations: 31 | - it removes singleton dimensions before calling zoom 32 | - if it detects that the zoom is only on the last two dimensions 33 | it stacks the first dimensions to call zoom only on the last two. 34 | """ 35 | mask = np.isclose(x.shape, 1) 36 | zoom = np.array(zoom) 37 | singletons = tuple(np.where(mask)[0]) 38 | xs = np.squeeze(x, axis=singletons) 39 | new_zoom = zoom[~mask] 40 | 41 | *zoom_rest, zoom_y, zoom_x = new_zoom 42 | if auto_stack and np.allclose(zoom_rest, 1): 43 | xs = _stacked_zoom( 44 | xs, zoom_y, zoom_x, order=order, mode=mode, grid_mode=grid_mode 45 | ) 46 | else: 47 | xs = scipy_zoom(xs, new_zoom, order=order, mode=mode, grid_mode=grid_mode) 48 | x = np.expand_dims(xs, axis=singletons) 49 | return x 50 | 51 | 52 | def _zoom_inputs_check( 53 | source_array: np.ndarray | da.Array, 54 | scale: tuple[int, ...] | None = None, 55 | target_shape: tuple[int, ...] | None = None, 56 | ) -> tuple[np.ndarray, tuple[int, ...]]: 57 | if scale is None and target_shape is None: 58 | raise NgioValueError("Either scale or target_shape must be provided") 59 | 60 | if scale is not None and target_shape is not None: 61 | raise NgioValueError("Only one of scale or target_shape must be provided") 62 | 63 | if scale is None: 64 | assert target_shape is not None, "Target shape must be provided" 65 | if len(target_shape) != source_array.ndim: 66 | raise NgioValueError( 67 | "Target shape must have the " 68 | "same number of dimensions as " 69 | "the source array" 70 | ) 71 | _scale = np.array(target_shape) / np.array(source_array.shape) 72 | _target_shape = target_shape 73 | else: 74 | _scale = np.array(scale) 75 | _target_shape = tuple(np.array(source_array.shape) * scale) 76 | 77 | return _scale, _target_shape 78 | 79 | 80 | def dask_zoom( 81 | source_array: da.Array, 82 | scale: tuple[int, ...] | None = None, 83 | target_shape: tuple[int, ...] | None = None, 84 | order: Literal[0, 1, 2] = 1, 85 | ) -> da.Array: 86 | """Dask implementation of zooming an array. 87 | 88 | Only one of scale or target_shape must be provided. 89 | 90 | Args: 91 | source_array (da.Array): The source array to zoom. 92 | scale (tuple[int, ...] | None): The scale factor to zoom by. 93 | target_shape (tuple[int, ...], None): The target shape to zoom to. 94 | order (Literal[0, 1, 2]): The order of interpolation. Defaults to 1. 95 | 96 | Returns: 97 | da.Array: The zoomed array. 98 | """ 99 | # This function follow the implementation from: 100 | # https://github.com/ome/ome-zarr-py/blob/master/ome_zarr/dask_utils.py 101 | # The module was contributed by Andreas Eisenbarth @aeisenbarth 102 | # See https://github.com/toloudis/ome-zarr-py/pull/ 103 | 104 | _scale, _target_shape = _zoom_inputs_check( 105 | source_array=source_array, scale=scale, target_shape=target_shape 106 | ) 107 | 108 | # Rechunk to better match the scaling operation 109 | source_chunks = np.array(source_array.chunksize) 110 | better_source_chunks = np.maximum(1, np.round(source_chunks * _scale) / _scale) 111 | better_source_chunks = better_source_chunks.astype(int) 112 | source_array = source_array.rechunk(better_source_chunks) # type: ignore 113 | 114 | # Calculate the block output shape 115 | block_output_shape = tuple(np.ceil(better_source_chunks * _scale).astype(int)) 116 | 117 | zoom_wrapper = partial( 118 | fast_zoom, zoom=_scale, order=order, mode="grid-constant", grid_mode=True 119 | ) 120 | 121 | out_array = da.map_blocks( 122 | zoom_wrapper, source_array, chunks=block_output_shape, dtype=source_array.dtype 123 | ) 124 | 125 | # Slice and rechunk to target 126 | slices = tuple(slice(0, ts, 1) for ts in _target_shape) 127 | out_array = out_array[slices] 128 | return out_array 129 | 130 | 131 | def numpy_zoom( 132 | source_array: np.ndarray, 133 | scale: tuple[int, ...] | None = None, 134 | target_shape: tuple[int, ...] | None = None, 135 | order: Literal[0, 1, 2] = 1, 136 | ) -> np.ndarray: 137 | """Numpy implementation of zooming an array. 138 | 139 | Only one of scale or target_shape must be provided. 140 | 141 | Args: 142 | source_array (np.ndarray): The source array to zoom. 143 | scale (tuple[int, ...] | None): The scale factor to zoom by. 144 | target_shape (tuple[int, ...], None): The target shape to zoom to. 145 | order (Literal[0, 1, 2]): The order of interpolation. Defaults to 1. 146 | 147 | Returns: 148 | np.ndarray: The zoomed array 149 | """ 150 | _scale, _ = _zoom_inputs_check( 151 | source_array=source_array, scale=scale, target_shape=target_shape 152 | ) 153 | 154 | out_array = fast_zoom( 155 | source_array, zoom=_scale, order=order, mode="grid-constant", grid_mode=True 156 | ) 157 | assert isinstance(out_array, np.ndarray) 158 | return out_array 159 | -------------------------------------------------------------------------------- /src/ngio/hcs/__init__.py: -------------------------------------------------------------------------------- 1 | """OME-Zarr HCS objects models.""" 2 | 3 | from ngio.hcs.plate import ( 4 | OmeZarrPlate, 5 | OmeZarrWell, 6 | create_empty_plate, 7 | create_empty_well, 8 | open_ome_zarr_plate, 9 | open_ome_zarr_well, 10 | ) 11 | 12 | __all__ = [ 13 | "OmeZarrPlate", 14 | "OmeZarrWell", 15 | "create_empty_plate", 16 | "create_empty_well", 17 | "open_ome_zarr_plate", 18 | "open_ome_zarr_well", 19 | ] 20 | -------------------------------------------------------------------------------- /src/ngio/images/__init__.py: -------------------------------------------------------------------------------- 1 | """OME-Zarr object models.""" 2 | 3 | from ngio.images.image import Image, ImagesContainer 4 | from ngio.images.label import Label, LabelsContainer 5 | from ngio.images.ome_zarr_container import ( 6 | OmeZarrContainer, 7 | create_empty_ome_zarr, 8 | create_ome_zarr_from_array, 9 | open_image, 10 | open_ome_zarr_container, 11 | ) 12 | 13 | __all__ = [ 14 | "Image", 15 | "ImagesContainer", 16 | "Label", 17 | "LabelsContainer", 18 | "OmeZarrContainer", 19 | "create_empty_ome_zarr", 20 | "create_ome_zarr_from_array", 21 | "open_image", 22 | "open_ome_zarr_container", 23 | ] 24 | -------------------------------------------------------------------------------- /src/ngio/ome_zarr_meta/__init__.py: -------------------------------------------------------------------------------- 1 | """Utilities for reading and writing OME-Zarr metadata.""" 2 | 3 | from ngio.ome_zarr_meta._meta_handlers import ( 4 | ImageMetaHandler, 5 | LabelMetaHandler, 6 | find_image_meta_handler, 7 | find_label_meta_handler, 8 | find_plate_meta_handler, 9 | find_well_meta_handler, 10 | get_image_meta_handler, 11 | get_label_meta_handler, 12 | get_plate_meta_handler, 13 | get_well_meta_handler, 14 | ) 15 | from ngio.ome_zarr_meta.ngio_specs import ( 16 | AxesMapper, 17 | Dataset, 18 | ImageInWellPath, 19 | NgffVersions, 20 | NgioImageMeta, 21 | NgioLabelMeta, 22 | NgioPlateMeta, 23 | NgioWellMeta, 24 | PixelSize, 25 | path_in_well_validation, 26 | ) 27 | 28 | __all__ = [ 29 | "AxesMapper", 30 | "Dataset", 31 | "ImageInWellPath", 32 | "ImageMetaHandler", 33 | "ImageMetaHandler", 34 | "LabelMetaHandler", 35 | "LabelMetaHandler", 36 | "NgffVersions", 37 | "NgffVersions", 38 | "NgioImageMeta", 39 | "NgioLabelMeta", 40 | "NgioPlateMeta", 41 | "NgioWellMeta", 42 | "PixelSize", 43 | "find_image_meta_handler", 44 | "find_label_meta_handler", 45 | "find_plate_meta_handler", 46 | "find_well_meta_handler", 47 | "get_image_meta_handler", 48 | "get_label_meta_handler", 49 | "get_plate_meta_handler", 50 | "get_well_meta_handler", 51 | "path_in_well_validation", 52 | ] 53 | -------------------------------------------------------------------------------- /src/ngio/ome_zarr_meta/ngio_specs/__init__.py: -------------------------------------------------------------------------------- 1 | """ngio internal specs module. 2 | 3 | Since the OME-Zarr specification are still evolving, this module provides a 4 | set of classes to internally handle the metadata. 5 | 6 | This models can be tr 7 | """ 8 | 9 | from ngio.ome_zarr_meta.ngio_specs._axes import ( 10 | AxesExpand, 11 | AxesMapper, 12 | AxesSetup, 13 | AxesSqueeze, 14 | AxesTransformation, 15 | AxesTranspose, 16 | Axis, 17 | AxisType, 18 | DefaultSpaceUnit, 19 | DefaultTimeUnit, 20 | SpaceUnits, 21 | TimeUnits, 22 | canonical_axes_order, 23 | canonical_label_axes_order, 24 | ) 25 | from ngio.ome_zarr_meta.ngio_specs._channels import ( 26 | Channel, 27 | ChannelsMeta, 28 | ChannelVisualisation, 29 | NgioColors, 30 | default_channel_name, 31 | ) 32 | from ngio.ome_zarr_meta.ngio_specs._dataset import Dataset 33 | from ngio.ome_zarr_meta.ngio_specs._ngio_hcs import ( 34 | ImageInWellPath, 35 | NgioPlateMeta, 36 | NgioWellMeta, 37 | path_in_well_validation, 38 | ) 39 | from ngio.ome_zarr_meta.ngio_specs._ngio_image import ( 40 | DefaultNgffVersion, 41 | ImageLabelSource, 42 | NgffVersions, 43 | NgioImageLabelMeta, 44 | NgioImageMeta, 45 | NgioLabelMeta, 46 | ) 47 | from ngio.ome_zarr_meta.ngio_specs._pixel_size import PixelSize 48 | 49 | __all__ = [ 50 | "AxesExpand", 51 | "AxesMapper", 52 | "AxesSetup", 53 | "AxesSqueeze", 54 | "AxesTransformation", 55 | "AxesTranspose", 56 | "Axis", 57 | "AxisType", 58 | "Channel", 59 | "ChannelVisualisation", 60 | "ChannelsMeta", 61 | "Dataset", 62 | "DefaultNgffVersion", 63 | "DefaultSpaceUnit", 64 | "DefaultTimeUnit", 65 | "ImageInWellPath", 66 | "ImageLabelSource", 67 | "NgffVersions", 68 | "NgioColors", 69 | "NgioImageLabelMeta", 70 | "NgioImageMeta", 71 | "NgioLabelMeta", 72 | "NgioPlateMeta", 73 | "NgioWellMeta", 74 | "PixelSize", 75 | "SpaceUnits", 76 | "TimeUnits", 77 | "canonical_axes_order", 78 | "canonical_label_axes_order", 79 | "default_channel_name", 80 | "path_in_well_validation", 81 | ] 82 | -------------------------------------------------------------------------------- /src/ngio/ome_zarr_meta/ngio_specs/_pixel_size.py: -------------------------------------------------------------------------------- 1 | """Fractal internal module for dataset metadata handling.""" 2 | 3 | import math 4 | from functools import total_ordering 5 | 6 | import numpy as np 7 | 8 | from ngio.ome_zarr_meta.ngio_specs import ( 9 | DefaultSpaceUnit, 10 | DefaultTimeUnit, 11 | SpaceUnits, 12 | TimeUnits, 13 | ) 14 | 15 | ################################################################################################ 16 | # 17 | # PixelSize model 18 | # The PixelSize model is used to store the pixel size in 3D space. 19 | # The model does not store scaling factors and units for other axes. 20 | # 21 | ################################################################################################# 22 | 23 | 24 | def _validate_type(value: float, name: str) -> float: 25 | """Check the type of the value.""" 26 | if not isinstance(value, int | float): 27 | raise TypeError(f"{name} must be a number.") 28 | return float(value) 29 | 30 | 31 | @total_ordering 32 | class PixelSize: 33 | """PixelSize class to store the pixel size in 3D space.""" 34 | 35 | def __init__( 36 | self, 37 | x: float, 38 | y: float, 39 | z: float, 40 | t: float = 1, 41 | space_unit: SpaceUnits | str | None = DefaultSpaceUnit, 42 | time_unit: TimeUnits | str | None = DefaultTimeUnit, 43 | ): 44 | """Initialize the pixel size.""" 45 | self.x = _validate_type(x, "x") 46 | self.y = _validate_type(y, "y") 47 | self.z = _validate_type(z, "z") 48 | self.t = _validate_type(t, "t") 49 | 50 | self._space_unit = space_unit 51 | self._time_unit = time_unit 52 | 53 | def __repr__(self) -> str: 54 | """Return a string representation of the pixel size.""" 55 | return f"PixelSize(x={self.x}, y={self.y}, z={self.z}, t={self.t})" 56 | 57 | def __eq__(self, other) -> bool: 58 | """Check if two pixel sizes are equal.""" 59 | if not isinstance(other, PixelSize): 60 | raise TypeError("Can only compare PixelSize with PixelSize.") 61 | 62 | if ( 63 | self.time_unit is not None 64 | and other.time_unit is None 65 | and self.time_unit != other.time_unit 66 | ): 67 | return False 68 | 69 | if self.space_unit != other.space_unit: 70 | return False 71 | return math.isclose(self.distance(other), 0) 72 | 73 | def __lt__(self, other: "PixelSize") -> bool: 74 | """Check if one pixel size is less than the other.""" 75 | if not isinstance(other, PixelSize): 76 | raise TypeError("Can only compare PixelSize with PixelSize.") 77 | ref = PixelSize( 78 | 0, 79 | 0, 80 | 0, 81 | 0, 82 | space_unit=self.space_unit, 83 | time_unit=self.time_unit, # type: ignore 84 | ) 85 | return self.distance(ref) < other.distance(ref) 86 | 87 | def as_dict(self) -> dict: 88 | """Return the pixel size as a dictionary.""" 89 | return {"t": self.t, "z": self.z, "y": self.y, "x": self.x} 90 | 91 | @property 92 | def space_unit(self) -> SpaceUnits | str | None: 93 | """Return the space unit.""" 94 | return self._space_unit 95 | 96 | @property 97 | def time_unit(self) -> TimeUnits | str | None: 98 | """Return the time unit.""" 99 | return self._time_unit 100 | 101 | @property 102 | def tzyx(self) -> tuple[float, float, float, float]: 103 | """Return the voxel size in t, z, y, x order.""" 104 | return self.t, self.z, self.y, self.x 105 | 106 | @property 107 | def zyx(self) -> tuple[float, float, float]: 108 | """Return the voxel size in z, y, x order.""" 109 | return self.z, self.y, self.x 110 | 111 | @property 112 | def yx(self) -> tuple[float, float]: 113 | """Return the xy plane pixel size in y, x order.""" 114 | return self.y, self.x 115 | 116 | @property 117 | def voxel_volume(self) -> float: 118 | """Return the volume of a voxel.""" 119 | return self.y * self.x * self.z 120 | 121 | @property 122 | def xy_plane_area(self) -> float: 123 | """Return the area of the xy plane.""" 124 | return self.y * self.x 125 | 126 | @property 127 | def time_spacing(self) -> float | None: 128 | """Return the time spacing.""" 129 | return self.t 130 | 131 | def distance(self, other: "PixelSize") -> float: 132 | """Return the distance between two pixel sizes.""" 133 | return float(np.linalg.norm(np.array(self.tzyx) - np.array(other.tzyx))) 134 | -------------------------------------------------------------------------------- /src/ngio/ome_zarr_meta/v04/__init__.py: -------------------------------------------------------------------------------- 1 | """Utility to read/write OME-Zarr metadata v0.4.""" 2 | 3 | from ngio.ome_zarr_meta.v04._v04_spec_utils import ( 4 | ngio_to_v04_image_meta, 5 | ngio_to_v04_label_meta, 6 | ngio_to_v04_plate_meta, 7 | ngio_to_v04_well_meta, 8 | v04_to_ngio_image_meta, 9 | v04_to_ngio_label_meta, 10 | v04_to_ngio_plate_meta, 11 | v04_to_ngio_well_meta, 12 | ) 13 | 14 | __all__ = [ 15 | "ngio_to_v04_image_meta", 16 | "ngio_to_v04_label_meta", 17 | "ngio_to_v04_plate_meta", 18 | "ngio_to_v04_well_meta", 19 | "v04_to_ngio_image_meta", 20 | "v04_to_ngio_label_meta", 21 | "v04_to_ngio_plate_meta", 22 | "v04_to_ngio_well_meta", 23 | ] 24 | -------------------------------------------------------------------------------- /src/ngio/ome_zarr_meta/v04/_custom_models.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from ome_zarr_models.v04.well import WellAttrs as WellAttrs04 4 | from ome_zarr_models.v04.well_types import WellImage as WellImage04 5 | from ome_zarr_models.v04.well_types import WellMeta as WellMeta04 6 | from pydantic import SkipValidation 7 | 8 | 9 | class CustomWellImage(WellImage04): 10 | path: Annotated[str, SkipValidation] 11 | 12 | 13 | class CustomWellMeta(WellMeta04): 14 | images: list[CustomWellImage] # type: ignore[valid-type] 15 | 16 | 17 | class CustomWellAttrs(WellAttrs04): 18 | well: CustomWellMeta # type: ignore[valid-type] 19 | -------------------------------------------------------------------------------- /src/ngio/tables/__init__.py: -------------------------------------------------------------------------------- 1 | """Ngio Tables implementations.""" 2 | 3 | from ngio.tables.backends import ImplementedTableBackends 4 | from ngio.tables.tables_container import ( 5 | FeatureTable, 6 | GenericRoiTable, 7 | MaskingRoiTable, 8 | RoiTable, 9 | Table, 10 | TablesContainer, 11 | TypedTable, 12 | open_table, 13 | open_tables_container, 14 | ) 15 | from ngio.tables.v1._generic_table import GenericTable 16 | 17 | __all__ = [ 18 | "FeatureTable", 19 | "GenericRoiTable", 20 | "GenericTable", 21 | "ImplementedTableBackends", 22 | "MaskingRoiTable", 23 | "RoiTable", 24 | "Table", 25 | "TablesContainer", 26 | "TypedTable", 27 | "open_table", 28 | "open_tables_container", 29 | ] 30 | -------------------------------------------------------------------------------- /src/ngio/tables/_validators.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Iterable 2 | from typing import Protocol 3 | 4 | import pandas as pd 5 | 6 | from ngio.utils import NgioTableValidationError 7 | 8 | 9 | class TableValidator(Protocol): 10 | def __call__(self, table: pd.DataFrame) -> pd.DataFrame: 11 | """Validate the table DataFrame. 12 | 13 | A Validator is just a simple callable that takes a 14 | DataFrame and returns a DataFrame. 15 | 16 | If the DataFrame is valid, the same DataFrame is returned. 17 | If the DataFrame is invalid, the Validator can either modify the DataFrame 18 | to make it valid or raise a NgioTableValidationError. 19 | 20 | Args: 21 | table (pd.DataFrame): The DataFrame to validate. 22 | 23 | Returns: 24 | pd.DataFrame: The validated DataFrame. 25 | 26 | """ 27 | ... 28 | 29 | 30 | def validate_table( 31 | table_df: pd.DataFrame, 32 | validators: Iterable[TableValidator] | None = None, 33 | ) -> pd.DataFrame: 34 | """Validate the table DataFrame. 35 | 36 | Args: 37 | table_df (pd.DataFrame): The DataFrame to validate. 38 | validators (Collection[Validator] | None): A collection of functions 39 | used to validate the table. Default is None. 40 | 41 | Returns: 42 | pd.DataFrame: The validated DataFrame. 43 | """ 44 | validators = validators or [] 45 | 46 | # Apply all provided validators 47 | for validator in validators: 48 | table_df = validator(table_df) 49 | 50 | return table_df 51 | 52 | 53 | #################################################################################################### 54 | # 55 | # Common table validators 56 | # 57 | #################################################################################################### 58 | def validate_columns( 59 | table_df: pd.DataFrame, 60 | required_columns: list[str], 61 | optional_columns: list[str] | None = None, 62 | ) -> pd.DataFrame: 63 | """Validate the columns headers of the table. 64 | 65 | If a required column is missing, a TableValidationError is raised. 66 | If a list of optional columns is provided, only required and optional columns are 67 | allowed in the table. 68 | 69 | Args: 70 | table_df (pd.DataFrame): The DataFrame to validate. 71 | required_columns (list[str]): A list of required columns. 72 | optional_columns (list[str] | None): A list of optional columns. 73 | Default is None. 74 | 75 | Returns: 76 | pd.DataFrame: The validated DataFrame. 77 | """ 78 | table_header = table_df.columns 79 | for column in required_columns: 80 | if column not in table_header: 81 | raise NgioTableValidationError( 82 | f"Could not find required column: {column} in the table" 83 | ) 84 | 85 | if optional_columns is None: 86 | return table_df 87 | 88 | possible_columns = [*required_columns, *optional_columns] 89 | for column in table_header: 90 | if column not in possible_columns: 91 | raise NgioTableValidationError( 92 | f"Could not find column: {column} in the list of possible columns. ", 93 | f"Possible columns are: {possible_columns}", 94 | ) 95 | 96 | return table_df 97 | 98 | 99 | def validate_unique_index(table_df: pd.DataFrame) -> pd.DataFrame: 100 | """Validate that the index of the table is unique.""" 101 | if table_df.index.is_unique: 102 | return table_df 103 | 104 | # Find the duplicates 105 | duplicates = table_df.index[table_df.index.duplicated()].tolist() 106 | raise NgioTableValidationError( 107 | f"Index of the table contains duplicates values. Duplicate: {duplicates}" 108 | ) 109 | -------------------------------------------------------------------------------- /src/ngio/tables/backends/__init__.py: -------------------------------------------------------------------------------- 1 | """Ngio Tables backend implementations.""" 2 | 3 | from ngio.tables.backends._abstract_backend import AbstractTableBackend, BackendMeta 4 | from ngio.tables.backends._table_backends import ( 5 | ImplementedTableBackends, 6 | TableBackendProtocol, 7 | ) 8 | from ngio.tables.backends._utils import ( 9 | convert_anndata_to_pandas, 10 | convert_anndata_to_polars, 11 | convert_pandas_to_anndata, 12 | convert_pandas_to_polars, 13 | convert_polars_to_anndata, 14 | convert_polars_to_pandas, 15 | normalize_anndata, 16 | normalize_pandas_df, 17 | normalize_polars_lf, 18 | ) 19 | 20 | __all__ = [ 21 | "AbstractTableBackend", 22 | "BackendMeta", 23 | "ImplementedTableBackends", 24 | "TableBackendProtocol", 25 | "convert_anndata_to_pandas", 26 | "convert_anndata_to_polars", 27 | "convert_pandas_to_anndata", 28 | "convert_pandas_to_polars", 29 | "convert_polars_to_anndata", 30 | "convert_polars_to_pandas", 31 | "normalize_anndata", 32 | "normalize_pandas_df", 33 | "normalize_polars_lf", 34 | ] 35 | -------------------------------------------------------------------------------- /src/ngio/tables/backends/_anndata_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any 4 | 5 | import zarr 6 | from anndata import AnnData 7 | from anndata._io.specs import read_elem 8 | from anndata._io.utils import _read_legacy_raw 9 | from anndata._io.zarr import read_dataframe 10 | from anndata.compat import _clean_uns 11 | from anndata.experimental import read_dispatched 12 | 13 | from ngio.utils import ( 14 | NgioValueError, 15 | StoreOrGroup, 16 | open_group_wrapper, 17 | ) 18 | 19 | if TYPE_CHECKING: 20 | from collections.abc import Callable, Collection 21 | 22 | 23 | def custom_anndata_read_zarr( 24 | store: StoreOrGroup, elem_to_read: Collection[str] | None = None 25 | ) -> AnnData: 26 | """Read from a hierarchical Zarr array store. 27 | 28 | # Implementation originally from https://github.com/scverse/anndata/blob/main/src/anndata/_io/zarr.py 29 | # Original implementation would not work with remote storages so we had to copy it 30 | # here and slightly modified it to work with remote storages. 31 | 32 | Args: 33 | store (StoreOrGroup): A store or group to read the AnnData from. 34 | elem_to_read (Collection[str] | None): The elements to read from the store. 35 | """ 36 | group = open_group_wrapper(store=store, mode="r") 37 | 38 | if not isinstance(group.store, zarr.DirectoryStore): 39 | elem_to_read = ["X", "obs", "var"] 40 | 41 | if elem_to_read is None: 42 | elem_to_read = [ 43 | "X", 44 | "obs", 45 | "var", 46 | "uns", 47 | "obsm", 48 | "varm", 49 | "obsp", 50 | "varp", 51 | "layers", 52 | ] 53 | 54 | # Read with handling for backwards compat 55 | def callback(func: Callable, elem_name: str, elem: Any, iospec: Any) -> Any: 56 | if iospec.encoding_type == "anndata" or elem_name.endswith("/"): 57 | ad_kwargs = {} 58 | # Some of these elem fail on https 59 | # So we only include the ones that are strictly necessary 60 | # for fractal tables 61 | # This fails on some https 62 | # base_elem += list(elem.keys()) 63 | for k in elem_to_read: 64 | v = elem.get(k) 65 | if v is not None and not k.startswith("raw."): 66 | ad_kwargs[k] = read_dispatched(v, callback) # type: ignore 67 | return AnnData(**ad_kwargs) 68 | 69 | elif elem_name.startswith("/raw."): 70 | return None 71 | elif elem_name in {"/obs", "/var"}: 72 | return read_dataframe(elem) 73 | elif elem_name == "/raw": 74 | # Backwards compat 75 | return _read_legacy_raw(group, func(elem), read_dataframe, func) 76 | return func(elem) 77 | 78 | adata = read_dispatched(group, callback=callback) # type: ignore 79 | 80 | # Backwards compat (should figure out which version) 81 | if "raw.X" in group: 82 | raw = AnnData(**_read_legacy_raw(group, adata.raw, read_dataframe, read_elem)) # type: ignore 83 | raw.obs_names = adata.obs_names # type: ignore 84 | adata.raw = raw # type: ignore 85 | 86 | # Backwards compat for <0.7 87 | if isinstance(group["obs"], zarr.Array): 88 | _clean_uns(adata) 89 | 90 | if not isinstance(adata, AnnData): 91 | raise NgioValueError(f"Expected an AnnData object, but got {type(adata)}") 92 | return adata 93 | -------------------------------------------------------------------------------- /src/ngio/tables/backends/_anndata_v1.py: -------------------------------------------------------------------------------- 1 | from anndata import AnnData 2 | from pandas import DataFrame 3 | from polars import DataFrame as PolarsDataFrame 4 | from polars import LazyFrame 5 | 6 | from ngio.tables.backends._abstract_backend import AbstractTableBackend 7 | from ngio.tables.backends._anndata_utils import ( 8 | custom_anndata_read_zarr, 9 | ) 10 | from ngio.tables.backends._utils import ( 11 | convert_pandas_to_anndata, 12 | convert_polars_to_anndata, 13 | normalize_anndata, 14 | ) 15 | from ngio.utils import NgioValueError 16 | 17 | 18 | class AnnDataBackend(AbstractTableBackend): 19 | """A class to load and write tables from/to an AnnData object.""" 20 | 21 | @staticmethod 22 | def backend_name() -> str: 23 | """Return the name of the backend.""" 24 | return "anndata_v1" 25 | 26 | @staticmethod 27 | def implements_anndata() -> bool: 28 | """Check if the backend implements the anndata protocol.""" 29 | return True 30 | 31 | @staticmethod 32 | def implements_pandas() -> bool: 33 | """Whether the handler implements the dataframe protocol.""" 34 | return True 35 | 36 | @staticmethod 37 | def implements_polars() -> bool: 38 | """Whether the handler implements the polars protocol.""" 39 | return True 40 | 41 | def load_as_anndata(self) -> AnnData: 42 | """Load the table as an AnnData object.""" 43 | anndata = custom_anndata_read_zarr(self._group_handler._group) 44 | anndata = normalize_anndata(anndata, index_key=self.index_key) 45 | return anndata 46 | 47 | def write_from_anndata(self, table: AnnData) -> None: 48 | """Serialize the table from an AnnData object.""" 49 | full_url = self._group_handler.full_url 50 | if full_url is None: 51 | raise NgioValueError( 52 | f"Ngio does not support writing file from a " 53 | f"store of type {type(self._group_handler)}. " 54 | "Please make sure to use a compatible " 55 | "store like a zarr.DirectoryStore." 56 | ) 57 | table.write_zarr(full_url) # type: ignore 58 | 59 | def write_from_pandas(self, table: DataFrame) -> None: 60 | """Serialize the table from a pandas DataFrame.""" 61 | anndata = convert_pandas_to_anndata( 62 | table, 63 | index_key=self.index_key, 64 | ) 65 | self.write_from_anndata(anndata) 66 | 67 | def write_from_polars(self, table: PolarsDataFrame | LazyFrame) -> None: 68 | """Consolidate the metadata in the store.""" 69 | anndata = convert_polars_to_anndata( 70 | table, 71 | index_key=self.index_key, 72 | ) 73 | self.write_from_anndata(anndata) 74 | -------------------------------------------------------------------------------- /src/ngio/tables/backends/_json_v1.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from pandas import DataFrame 3 | from polars import DataFrame as PolarsDataFrame 4 | from polars import LazyFrame 5 | 6 | from ngio.tables.backends._abstract_backend import AbstractTableBackend 7 | from ngio.tables.backends._utils import ( 8 | normalize_pandas_df, 9 | normalize_polars_lf, 10 | ) 11 | from ngio.utils import NgioFileNotFoundError 12 | 13 | 14 | class JsonTableBackend(AbstractTableBackend): 15 | """A class to load and write small tables in the zarr group .attrs (json).""" 16 | 17 | @staticmethod 18 | def backend_name() -> str: 19 | """Return the name of the backend.""" 20 | return "experimental_json_v1" 21 | 22 | @staticmethod 23 | def implements_anndata() -> bool: 24 | """Whether the handler implements the anndata protocol.""" 25 | return False 26 | 27 | @staticmethod 28 | def implements_pandas() -> bool: 29 | """Whether the handler implements the dataframe protocol.""" 30 | return True 31 | 32 | @staticmethod 33 | def implements_polars() -> bool: 34 | """Whether the handler implements the polars protocol.""" 35 | return True 36 | 37 | def _get_table_group(self): 38 | """Get the table group, creating it if it doesn't exist.""" 39 | try: 40 | table_group = self._group_handler.get_group(path="table") 41 | except NgioFileNotFoundError: 42 | table_group = self._group_handler.group.create_group("table") 43 | return table_group 44 | 45 | def _load_as_pandas_df(self) -> DataFrame: 46 | """Load the table as a pandas DataFrame.""" 47 | table_group = self._get_table_group() 48 | table_dict = dict(table_group.attrs) 49 | 50 | data_frame = pd.DataFrame.from_dict(table_dict) 51 | return data_frame 52 | 53 | def load_as_pandas_df(self) -> DataFrame: 54 | """Load the table as a pandas DataFrame.""" 55 | data_frame = self._load_as_pandas_df() 56 | data_frame = normalize_pandas_df( 57 | data_frame, 58 | index_key=self.index_key, 59 | index_type=self.index_type, 60 | reset_index=False, 61 | ) 62 | return data_frame 63 | 64 | def _write_from_dict(self, table: dict) -> None: 65 | """Write the table from a dictionary to the store.""" 66 | table_group = self._get_table_group() 67 | table_group.attrs.clear() 68 | table_group.attrs.update(table) 69 | 70 | def write_from_pandas(self, table: DataFrame) -> None: 71 | """Write the table from a pandas DataFrame.""" 72 | table = normalize_pandas_df( 73 | table, 74 | index_key=self.index_key, 75 | index_type=self.index_type, 76 | reset_index=True, 77 | ) 78 | table_dict = table.to_dict(orient="list") 79 | self._write_from_dict(table=table_dict) 80 | 81 | def write_from_polars(self, table: PolarsDataFrame | LazyFrame) -> None: 82 | """Write the table from a polars DataFrame or LazyFrame.""" 83 | table = normalize_polars_lf( 84 | table, 85 | index_key=self.index_key, 86 | index_type=self.index_type, 87 | ) 88 | if isinstance(table, LazyFrame): 89 | table = table.collect() 90 | 91 | table_dict = table.to_dict(as_series=False) 92 | self._write_from_dict(table=table_dict) 93 | -------------------------------------------------------------------------------- /src/ngio/tables/v1/__init__.py: -------------------------------------------------------------------------------- 1 | """Tables implementations for fractal_tables v1.""" 2 | 3 | from ngio.tables.v1._feature_table import FeatureTableV1 4 | from ngio.tables.v1._generic_table import GenericTable 5 | from ngio.tables.v1._roi_table import MaskingRoiTableV1, RoiTableV1 6 | 7 | __all__ = ["FeatureTableV1", "GenericTable", "MaskingRoiTableV1", "RoiTableV1"] 8 | -------------------------------------------------------------------------------- /src/ngio/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Various utilities for the ngio package.""" 2 | 3 | import os 4 | 5 | from ngio.common._common_types import ArrayLike 6 | from ngio.utils._datasets import download_ome_zarr_dataset, list_ome_zarr_datasets 7 | from ngio.utils._errors import ( 8 | NgioFileExistsError, 9 | NgioFileNotFoundError, 10 | NgioTableValidationError, 11 | NgioValidationError, 12 | NgioValueError, 13 | ) 14 | from ngio.utils._fractal_fsspec_store import fractal_fsspec_store 15 | from ngio.utils._logger import ngio_logger, set_logger_level 16 | from ngio.utils._zarr_utils import ( 17 | AccessModeLiteral, 18 | StoreOrGroup, 19 | ZarrGroupHandler, 20 | open_group_wrapper, 21 | ) 22 | 23 | set_logger_level(os.getenv("NGIO_LOGGER_LEVEL", "WARNING")) 24 | 25 | __all__ = [ 26 | # Zarr 27 | "AccessModeLiteral", 28 | "ArrayLike", 29 | # Errors 30 | "NgioFileExistsError", 31 | "NgioFileNotFoundError", 32 | "NgioTableValidationError", 33 | "NgioValidationError", 34 | "NgioValueError", 35 | "StoreOrGroup", 36 | "ZarrGroupHandler", 37 | # Datasets 38 | "download_ome_zarr_dataset", 39 | # Fractal 40 | "fractal_fsspec_store", 41 | "list_ome_zarr_datasets", 42 | # Logger 43 | "ngio_logger", 44 | "open_group_wrapper", 45 | "set_logger_level", 46 | ] 47 | -------------------------------------------------------------------------------- /src/ngio/utils/_datasets.py: -------------------------------------------------------------------------------- 1 | """Download testing OME-Zarr datasets.""" 2 | 3 | from pathlib import Path 4 | 5 | import pooch 6 | 7 | from ngio.utils._errors import NgioValueError 8 | 9 | _ome_zarr_zoo = { 10 | "CardiomyocyteTiny": { 11 | "url": "https://zenodo.org/records/13305156/files/20200812-CardiomyocyteDifferentiation14-Cycle1.zarr.zip", 12 | "known_hash": "md5:efc21fe8d4ea3abab76226d8c166452c", 13 | "fname": "20200812-CardiomyocyteDifferentiation14-Cycle1.zarr.zip", 14 | "processor": pooch.Unzip(extract_dir=""), 15 | }, 16 | "CardiomyocyteSmallMip": { 17 | "url": "https://zenodo.org/records/13305316/files/20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr.zip", 18 | "known_hash": "md5:3ed3ea898e0ed42d397da2e1dbe40750", 19 | "fname": "20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr.zip", 20 | "processor": pooch.Unzip(extract_dir=""), 21 | }, 22 | } 23 | 24 | 25 | def list_ome_zarr_datasets() -> list[str]: 26 | """List available OME-Zarr datasets.""" 27 | return list(_ome_zarr_zoo.keys()) 28 | 29 | 30 | def download_ome_zarr_dataset( 31 | dataset_name: str, 32 | download_dir: str | Path = "data", 33 | ) -> Path: 34 | """Download an OME-Zarr dataset. 35 | 36 | To list available datasets, use `list_ome_zarr_datasets`. 37 | 38 | Args: 39 | dataset_name (str): The dataset name. 40 | download_dir (str): The download directory. Defaults to "data". 41 | """ 42 | if dataset_name not in _ome_zarr_zoo: 43 | raise NgioValueError(f"Dataset {dataset_name} not found in the OME-Zarr zoo.") 44 | ome_zarr_url = _ome_zarr_zoo[dataset_name] 45 | pooch.retrieve( 46 | path=download_dir, 47 | **ome_zarr_url, 48 | ) 49 | path = Path(download_dir) / ome_zarr_url["fname"] 50 | 51 | if isinstance(ome_zarr_url["processor"], pooch.Unzip): 52 | path = path.with_suffix("") 53 | return path 54 | -------------------------------------------------------------------------------- /src/ngio/utils/_errors.py: -------------------------------------------------------------------------------- 1 | # Create a generic error class for the NGFF project 2 | 3 | 4 | class NgioError(Exception): 5 | """Base class for all errors in the NGFF project.""" 6 | 7 | pass 8 | 9 | 10 | class NgioFileNotFoundError(NgioError, FileNotFoundError): 11 | """Error raised when a file is not found.""" 12 | 13 | pass 14 | 15 | 16 | class NgioFileExistsError(NgioError, FileExistsError): 17 | """Error raised when a file already exists.""" 18 | 19 | pass 20 | 21 | 22 | class NgioValidationError(NgioError, ValueError): 23 | """Generic error raised when a file does not pass validation.""" 24 | 25 | pass 26 | 27 | 28 | class NgioTableValidationError(NgioError): 29 | """Error raised when a table does not pass validation.""" 30 | 31 | pass 32 | 33 | 34 | class NgioValueError(NgioError, ValueError): 35 | """Error raised when a value does not pass a run time test.""" 36 | 37 | pass 38 | -------------------------------------------------------------------------------- /src/ngio/utils/_fractal_fsspec_store.py: -------------------------------------------------------------------------------- 1 | import fsspec.implementations.http 2 | 3 | 4 | def fractal_fsspec_store( 5 | url: str, fractal_token: str | None = None, client_kwargs: dict | None = None 6 | ) -> fsspec.mapping.FSMap: 7 | """Simple function to get an http fsspec store from a url.""" 8 | client_kwargs = {} if client_kwargs is None else client_kwargs 9 | if fractal_token is not None: 10 | client_kwargs["headers"] = {"Authorization": f"Bearer {fractal_token}"} 11 | fs = fsspec.implementations.http.HTTPFileSystem(client_kwargs=client_kwargs) 12 | store = fs.get_mapper(url) 13 | return store 14 | -------------------------------------------------------------------------------- /src/ngio/utils/_logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from ngio.utils._errors import NgioValueError 4 | 5 | # Configure the logger 6 | ngio_logger = logging.getLogger("NgioLogger") 7 | ngio_logger.setLevel(logging.ERROR) 8 | 9 | # Set up a console handler with a custom format 10 | console_handler = logging.StreamHandler() 11 | formatter = logging.Formatter( 12 | "%(asctime)s - %(levelname)s - %(name)s - " 13 | "[%(module)s.%(funcName)s:%(lineno)d]: %(message)s" 14 | ) 15 | console_handler.setFormatter(formatter) 16 | 17 | # Add the handler to the logger 18 | ngio_logger.addHandler(console_handler) 19 | 20 | 21 | def set_logger_level(level: str) -> None: 22 | """Set the logger level. 23 | 24 | Args: 25 | level: The level to set the logger to. 26 | Must be one of "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL". 27 | """ 28 | if level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: 29 | raise NgioValueError(f"Invalid log level: {level}") 30 | 31 | ngio_logger.setLevel(level) 32 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from ngio.utils import download_ome_zarr_dataset 8 | 9 | zenodo_download_dir = Path(__file__).parent.parent / "data" 10 | os.makedirs(zenodo_download_dir, exist_ok=True) 11 | cardiomyocyte_tiny_source_path = download_ome_zarr_dataset( 12 | "CardiomyocyteTiny", download_dir=zenodo_download_dir 13 | ) 14 | 15 | cardiomyocyte_small_mip_source_path = download_ome_zarr_dataset( 16 | "CardiomyocyteSmallMip", download_dir=zenodo_download_dir 17 | ) 18 | 19 | 20 | @pytest.fixture 21 | def cardiomyocyte_tiny_path(tmp_path: Path) -> Path: 22 | dest_path = tmp_path / cardiomyocyte_tiny_source_path.stem 23 | shutil.copytree(cardiomyocyte_tiny_source_path, dest_path, dirs_exist_ok=True) 24 | return dest_path 25 | 26 | 27 | @pytest.fixture 28 | def cardiomyocyte_small_mip_path(tmp_path: Path) -> Path: 29 | dest_path = tmp_path / cardiomyocyte_small_mip_source_path.stem 30 | shutil.copytree(cardiomyocyte_small_mip_source_path, dest_path, dirs_exist_ok=True) 31 | return dest_path 32 | 33 | 34 | @pytest.fixture 35 | def images_v04(tmp_path: Path) -> dict[str, Path]: 36 | source = Path("tests/data/v04/images/") 37 | dest = tmp_path / "v04" / "images" 38 | dest.mkdir(parents=True, exist_ok=True) 39 | shutil.copytree(source, dest, dirs_exist_ok=True) 40 | return {file.name: file for file in dest.glob("*.zarr")} 41 | -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "c", 7 | "type": "channel" 8 | }, 9 | { 10 | "name": "z", 11 | "type": "space", 12 | "unit": "micrometer" 13 | }, 14 | { 15 | "name": "y", 16 | "type": "space", 17 | "unit": "micrometer" 18 | }, 19 | { 20 | "name": "x", 21 | "type": "space", 22 | "unit": "micrometer" 23 | } 24 | ], 25 | "datasets": [ 26 | { 27 | "coordinateTransformations": [ 28 | { 29 | "scale": [ 30 | 1.0, 31 | 1.0, 32 | 0.5, 33 | 0.5 34 | ], 35 | "type": "scale" 36 | } 37 | ], 38 | "path": "0" 39 | }, 40 | { 41 | "coordinateTransformations": [ 42 | { 43 | "scale": [ 44 | 1.0, 45 | 1.0, 46 | 1.0, 47 | 1.0 48 | ], 49 | "type": "scale" 50 | } 51 | ], 52 | "path": "1" 53 | } 54 | ], 55 | "version": "0.4" 56 | } 57 | ], 58 | "omero": { 59 | "channels": [ 60 | { 61 | "active": true, 62 | "color": "00FFFF", 63 | "label": "channel1", 64 | "wavelength_id": "channel1", 65 | "window": { 66 | "end": 255.0, 67 | "max": 255.0, 68 | "min": 0.0, 69 | "start": 0.0 70 | } 71 | }, 72 | { 73 | "active": true, 74 | "color": "00FFFF", 75 | "label": "channel2", 76 | "wavelength_id": "channel2", 77 | "window": { 78 | "end": 255.0, 79 | "max": 255.0, 80 | "min": 0.0, 81 | "start": 0.0 82 | } 83 | } 84 | ] 85 | } 86 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 2, 4 | 1, 5 | 64, 6 | 64 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 2, 22 | 1, 23 | 64, 24 | 64 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 2, 4 | 1, 5 | 32, 6 | 32 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 2, 22 | 1, 23 | 32, 24 | 32 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "z", 13 | "type": "space", 14 | "unit": "micrometer" 15 | }, 16 | { 17 | "name": "y", 18 | "type": "space", 19 | "unit": "micrometer" 20 | }, 21 | { 22 | "name": "x", 23 | "type": "space", 24 | "unit": "micrometer" 25 | } 26 | ], 27 | "datasets": [ 28 | { 29 | "coordinateTransformations": [ 30 | { 31 | "scale": [ 32 | 1.0, 33 | 0.5, 34 | 0.5 35 | ], 36 | "type": "scale" 37 | } 38 | ], 39 | "path": "0" 40 | }, 41 | { 42 | "coordinateTransformations": [ 43 | { 44 | "scale": [ 45 | 1.0, 46 | 1.0, 47 | 1.0 48 | ], 49 | "type": "scale" 50 | } 51 | ], 52 | "path": "1" 53 | } 54 | ], 55 | "name": "label", 56 | "version": "0.4" 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 1, 4 | 64, 5 | 64 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 1, 21 | 64, 22 | 64 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_c1yx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 1, 4 | 32, 5 | 32 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 1, 21 | 32, 22 | 32 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "c", 7 | "type": "channel" 8 | }, 9 | { 10 | "name": "y", 11 | "type": "space", 12 | "unit": "micrometer" 13 | }, 14 | { 15 | "name": "x", 16 | "type": "space", 17 | "unit": "micrometer" 18 | } 19 | ], 20 | "datasets": [ 21 | { 22 | "coordinateTransformations": [ 23 | { 24 | "scale": [ 25 | 1.0, 26 | 0.5, 27 | 0.5 28 | ], 29 | "type": "scale" 30 | } 31 | ], 32 | "path": "0" 33 | }, 34 | { 35 | "coordinateTransformations": [ 36 | { 37 | "scale": [ 38 | 1.0, 39 | 1.0, 40 | 1.0 41 | ], 42 | "type": "scale" 43 | } 44 | ], 45 | "path": "1" 46 | } 47 | ], 48 | "version": "0.4" 49 | } 50 | ], 51 | "omero": { 52 | "channels": [ 53 | { 54 | "active": true, 55 | "color": "00FFFF", 56 | "label": "channel1", 57 | "wavelength_id": "channel1", 58 | "window": { 59 | "end": 255.0, 60 | "max": 255.0, 61 | "min": 0.0, 62 | "start": 0.0 63 | } 64 | }, 65 | { 66 | "active": true, 67 | "color": "00FFFF", 68 | "label": "channel2", 69 | "wavelength_id": "channel2", 70 | "window": { 71 | "end": 255.0, 72 | "max": 255.0, 73 | "min": 0.0, 74 | "start": 0.0 75 | } 76 | } 77 | ] 78 | } 79 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 2, 4 | 64, 5 | 64 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 2, 21 | 64, 22 | 64 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 2, 4 | 32, 5 | 32 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 2, 21 | 32, 22 | 32 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "y", 13 | "type": "space", 14 | "unit": "micrometer" 15 | }, 16 | { 17 | "name": "x", 18 | "type": "space", 19 | "unit": "micrometer" 20 | } 21 | ], 22 | "datasets": [ 23 | { 24 | "coordinateTransformations": [ 25 | { 26 | "scale": [ 27 | 0.5, 28 | 0.5 29 | ], 30 | "type": "scale" 31 | } 32 | ], 33 | "path": "0" 34 | }, 35 | { 36 | "coordinateTransformations": [ 37 | { 38 | "scale": [ 39 | 1.0, 40 | 1.0 41 | ], 42 | "type": "scale" 43 | } 44 | ], 45 | "path": "1" 46 | } 47 | ], 48 | "name": "label", 49 | "version": "0.4" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 64, 4 | 64 5 | ], 6 | "compressor": { 7 | "blocksize": 0, 8 | "clevel": 5, 9 | "cname": "lz4", 10 | "id": "blosc", 11 | "shuffle": 1 12 | }, 13 | "dimension_separator": "/", 14 | "dtype": "|u1", 15 | "fill_value": 0, 16 | "filters": null, 17 | "order": "C", 18 | "shape": [ 19 | 64, 20 | 64 21 | ], 22 | "zarr_format": 2 23 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_cyx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 32, 4 | 32 5 | ], 6 | "compressor": { 7 | "blocksize": 0, 8 | "clevel": 5, 9 | "cname": "lz4", 10 | "id": "blosc", 11 | "shuffle": 1 12 | }, 13 | "dimension_separator": "/", 14 | "dtype": "|u1", 15 | "fill_value": 0, 16 | "filters": null, 17 | "order": "C", 18 | "shape": [ 19 | 32, 20 | 32 21 | ], 22 | "zarr_format": 2 23 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "c", 7 | "type": "channel" 8 | }, 9 | { 10 | "name": "z", 11 | "type": "space", 12 | "unit": "micrometer" 13 | }, 14 | { 15 | "name": "y", 16 | "type": "space", 17 | "unit": "micrometer" 18 | }, 19 | { 20 | "name": "x", 21 | "type": "space", 22 | "unit": "micrometer" 23 | } 24 | ], 25 | "datasets": [ 26 | { 27 | "coordinateTransformations": [ 28 | { 29 | "scale": [ 30 | 1.0, 31 | 2.0, 32 | 0.5, 33 | 0.5 34 | ], 35 | "type": "scale" 36 | } 37 | ], 38 | "path": "0" 39 | }, 40 | { 41 | "coordinateTransformations": [ 42 | { 43 | "scale": [ 44 | 1.0, 45 | 2.0, 46 | 1.0, 47 | 1.0 48 | ], 49 | "type": "scale" 50 | } 51 | ], 52 | "path": "1" 53 | } 54 | ], 55 | "version": "0.4" 56 | } 57 | ], 58 | "omero": { 59 | "channels": [ 60 | { 61 | "active": true, 62 | "color": "00FFFF", 63 | "label": "channel1", 64 | "wavelength_id": "channel1", 65 | "window": { 66 | "end": 255.0, 67 | "max": 255.0, 68 | "min": 0.0, 69 | "start": 0.0 70 | } 71 | }, 72 | { 73 | "active": true, 74 | "color": "00FFFF", 75 | "label": "channel2", 76 | "wavelength_id": "channel2", 77 | "window": { 78 | "end": 255.0, 79 | "max": 255.0, 80 | "min": 0.0, 81 | "start": 0.0 82 | } 83 | } 84 | ] 85 | } 86 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 2, 4 | 3, 5 | 64, 6 | 64 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 2, 22 | 3, 23 | 64, 24 | 64 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 2, 4 | 3, 5 | 32, 6 | 32 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 2, 22 | 3, 23 | 32, 24 | 32 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "z", 13 | "type": "space", 14 | "unit": "micrometer" 15 | }, 16 | { 17 | "name": "y", 18 | "type": "space", 19 | "unit": "micrometer" 20 | }, 21 | { 22 | "name": "x", 23 | "type": "space", 24 | "unit": "micrometer" 25 | } 26 | ], 27 | "datasets": [ 28 | { 29 | "coordinateTransformations": [ 30 | { 31 | "scale": [ 32 | 2.0, 33 | 0.5, 34 | 0.5 35 | ], 36 | "type": "scale" 37 | } 38 | ], 39 | "path": "0" 40 | }, 41 | { 42 | "coordinateTransformations": [ 43 | { 44 | "scale": [ 45 | 2.0, 46 | 1.0, 47 | 1.0 48 | ], 49 | "type": "scale" 50 | } 51 | ], 52 | "path": "1" 53 | } 54 | ], 55 | "name": "label", 56 | "version": "0.4" 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 3, 4 | 64, 5 | 64 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 3, 21 | 64, 22 | 64 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_czyx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 3, 4 | 32, 5 | 32 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 3, 21 | 32, 22 | 32 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "t", 7 | "type": "time", 8 | "unit": "seconds" 9 | }, 10 | { 11 | "name": "c", 12 | "type": "channel" 13 | }, 14 | { 15 | "name": "y", 16 | "type": "space", 17 | "unit": "micrometer" 18 | }, 19 | { 20 | "name": "x", 21 | "type": "space", 22 | "unit": "micrometer" 23 | } 24 | ], 25 | "datasets": [ 26 | { 27 | "coordinateTransformations": [ 28 | { 29 | "scale": [ 30 | 4.0, 31 | 1.0, 32 | 0.5, 33 | 0.5 34 | ], 35 | "type": "scale" 36 | } 37 | ], 38 | "path": "0" 39 | }, 40 | { 41 | "coordinateTransformations": [ 42 | { 43 | "scale": [ 44 | 4.0, 45 | 1.0, 46 | 1.0, 47 | 1.0 48 | ], 49 | "type": "scale" 50 | } 51 | ], 52 | "path": "1" 53 | } 54 | ], 55 | "version": "0.4" 56 | } 57 | ], 58 | "omero": { 59 | "channels": [ 60 | { 61 | "active": true, 62 | "color": "00FFFF", 63 | "label": "channel1", 64 | "wavelength_id": "channel1", 65 | "window": { 66 | "end": 255.0, 67 | "max": 255.0, 68 | "min": 0.0, 69 | "start": 0.0 70 | } 71 | }, 72 | { 73 | "active": true, 74 | "color": "00FFFF", 75 | "label": "channel2", 76 | "wavelength_id": "channel2", 77 | "window": { 78 | "end": 255.0, 79 | "max": 255.0, 80 | "min": 0.0, 81 | "start": 0.0 82 | } 83 | } 84 | ] 85 | } 86 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 2, 5 | 64, 6 | 64 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 4, 22 | 2, 23 | 64, 24 | 64 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 2, 5 | 32, 6 | 32 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 4, 22 | 2, 23 | 32, 24 | 32 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "t", 13 | "type": "time", 14 | "unit": "seconds" 15 | }, 16 | { 17 | "name": "y", 18 | "type": "space", 19 | "unit": "micrometer" 20 | }, 21 | { 22 | "name": "x", 23 | "type": "space", 24 | "unit": "micrometer" 25 | } 26 | ], 27 | "datasets": [ 28 | { 29 | "coordinateTransformations": [ 30 | { 31 | "scale": [ 32 | 4.0, 33 | 0.5, 34 | 0.5 35 | ], 36 | "type": "scale" 37 | } 38 | ], 39 | "path": "0" 40 | }, 41 | { 42 | "coordinateTransformations": [ 43 | { 44 | "scale": [ 45 | 4.0, 46 | 1.0, 47 | 1.0 48 | ], 49 | "type": "scale" 50 | } 51 | ], 52 | "path": "1" 53 | } 54 | ], 55 | "name": "label", 56 | "version": "0.4" 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 64, 5 | 64 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 4, 21 | 64, 22 | 64 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tcyx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 32, 5 | 32 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 4, 21 | 32, 22 | 32 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "t", 7 | "type": "time", 8 | "unit": "seconds" 9 | }, 10 | { 11 | "name": "c", 12 | "type": "channel" 13 | }, 14 | { 15 | "name": "z", 16 | "type": "space", 17 | "unit": "micrometer" 18 | }, 19 | { 20 | "name": "y", 21 | "type": "space", 22 | "unit": "micrometer" 23 | }, 24 | { 25 | "name": "x", 26 | "type": "space", 27 | "unit": "micrometer" 28 | } 29 | ], 30 | "datasets": [ 31 | { 32 | "coordinateTransformations": [ 33 | { 34 | "scale": [ 35 | 4.0, 36 | 1.0, 37 | 2.0, 38 | 0.5, 39 | 0.5 40 | ], 41 | "type": "scale" 42 | } 43 | ], 44 | "path": "0" 45 | }, 46 | { 47 | "coordinateTransformations": [ 48 | { 49 | "scale": [ 50 | 4.0, 51 | 1.0, 52 | 2.0, 53 | 1.0, 54 | 1.0 55 | ], 56 | "type": "scale" 57 | } 58 | ], 59 | "path": "1" 60 | } 61 | ], 62 | "version": "0.4" 63 | } 64 | ], 65 | "omero": { 66 | "channels": [ 67 | { 68 | "active": true, 69 | "color": "00FFFF", 70 | "label": "channel1", 71 | "wavelength_id": "channel1", 72 | "window": { 73 | "end": 255.0, 74 | "max": 255.0, 75 | "min": 0.0, 76 | "start": 0.0 77 | } 78 | }, 79 | { 80 | "active": true, 81 | "color": "00FFFF", 82 | "label": "channel2", 83 | "wavelength_id": "channel2", 84 | "window": { 85 | "end": 255.0, 86 | "max": 255.0, 87 | "min": 0.0, 88 | "start": 0.0 89 | } 90 | } 91 | ] 92 | } 93 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 2, 5 | 3, 6 | 64, 7 | 64 8 | ], 9 | "compressor": { 10 | "blocksize": 0, 11 | "clevel": 5, 12 | "cname": "lz4", 13 | "id": "blosc", 14 | "shuffle": 1 15 | }, 16 | "dimension_separator": "/", 17 | "dtype": "|u1", 18 | "fill_value": 0, 19 | "filters": null, 20 | "order": "C", 21 | "shape": [ 22 | 4, 23 | 2, 24 | 3, 25 | 64, 26 | 64 27 | ], 28 | "zarr_format": 2 29 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 2, 5 | 3, 6 | 32, 7 | 32 8 | ], 9 | "compressor": { 10 | "blocksize": 0, 11 | "clevel": 5, 12 | "cname": "lz4", 13 | "id": "blosc", 14 | "shuffle": 1 15 | }, 16 | "dimension_separator": "/", 17 | "dtype": "|u1", 18 | "fill_value": 0, 19 | "filters": null, 20 | "order": "C", 21 | "shape": [ 22 | 4, 23 | 2, 24 | 3, 25 | 32, 26 | 32 27 | ], 28 | "zarr_format": 2 29 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "t", 13 | "type": "time", 14 | "unit": "seconds" 15 | }, 16 | { 17 | "name": "z", 18 | "type": "space", 19 | "unit": "micrometer" 20 | }, 21 | { 22 | "name": "y", 23 | "type": "space", 24 | "unit": "micrometer" 25 | }, 26 | { 27 | "name": "x", 28 | "type": "space", 29 | "unit": "micrometer" 30 | } 31 | ], 32 | "datasets": [ 33 | { 34 | "coordinateTransformations": [ 35 | { 36 | "scale": [ 37 | 4.0, 38 | 2.0, 39 | 0.5, 40 | 0.5 41 | ], 42 | "type": "scale" 43 | } 44 | ], 45 | "path": "0" 46 | }, 47 | { 48 | "coordinateTransformations": [ 49 | { 50 | "scale": [ 51 | 4.0, 52 | 2.0, 53 | 1.0, 54 | 1.0 55 | ], 56 | "type": "scale" 57 | } 58 | ], 59 | "path": "1" 60 | } 61 | ], 62 | "name": "label", 63 | "version": "0.4" 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 3, 5 | 64, 6 | 64 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 4, 22 | 3, 23 | 64, 24 | 64 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tczyx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 3, 5 | 32, 6 | 32 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 4, 22 | 3, 23 | 32, 24 | 32 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "t", 7 | "type": "time", 8 | "unit": "seconds" 9 | }, 10 | { 11 | "name": "y", 12 | "type": "space", 13 | "unit": "micrometer" 14 | }, 15 | { 16 | "name": "x", 17 | "type": "space", 18 | "unit": "micrometer" 19 | } 20 | ], 21 | "datasets": [ 22 | { 23 | "coordinateTransformations": [ 24 | { 25 | "scale": [ 26 | 4.0, 27 | 0.5, 28 | 0.5 29 | ], 30 | "type": "scale" 31 | } 32 | ], 33 | "path": "0" 34 | }, 35 | { 36 | "coordinateTransformations": [ 37 | { 38 | "scale": [ 39 | 4.0, 40 | 1.0, 41 | 1.0 42 | ], 43 | "type": "scale" 44 | } 45 | ], 46 | "path": "1" 47 | } 48 | ], 49 | "version": "0.4" 50 | } 51 | ], 52 | "omero": { 53 | "channels": [ 54 | { 55 | "active": true, 56 | "color": "00FFFF", 57 | "label": "channel_0", 58 | "wavelength_id": "channel_0", 59 | "window": { 60 | "end": 255.0, 61 | "max": 255.0, 62 | "min": 0.0, 63 | "start": 0.0 64 | } 65 | } 66 | ] 67 | } 68 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 64, 5 | 64 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 4, 21 | 64, 22 | 64 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 32, 5 | 32 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 4, 21 | 32, 22 | 32 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "t", 13 | "type": "time", 14 | "unit": "seconds" 15 | }, 16 | { 17 | "name": "y", 18 | "type": "space", 19 | "unit": "micrometer" 20 | }, 21 | { 22 | "name": "x", 23 | "type": "space", 24 | "unit": "micrometer" 25 | } 26 | ], 27 | "datasets": [ 28 | { 29 | "coordinateTransformations": [ 30 | { 31 | "scale": [ 32 | 4.0, 33 | 0.5, 34 | 0.5 35 | ], 36 | "type": "scale" 37 | } 38 | ], 39 | "path": "0" 40 | }, 41 | { 42 | "coordinateTransformations": [ 43 | { 44 | "scale": [ 45 | 4.0, 46 | 1.0, 47 | 1.0 48 | ], 49 | "type": "scale" 50 | } 51 | ], 52 | "path": "1" 53 | } 54 | ], 55 | "name": "label", 56 | "version": "0.4" 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 64, 5 | 64 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 4, 21 | 64, 22 | 64 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tyx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 32, 5 | 32 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 4, 21 | 32, 22 | 32 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "t", 7 | "type": "time", 8 | "unit": "seconds" 9 | }, 10 | { 11 | "name": "z", 12 | "type": "space", 13 | "unit": "micrometer" 14 | }, 15 | { 16 | "name": "y", 17 | "type": "space", 18 | "unit": "micrometer" 19 | }, 20 | { 21 | "name": "x", 22 | "type": "space", 23 | "unit": "micrometer" 24 | } 25 | ], 26 | "datasets": [ 27 | { 28 | "coordinateTransformations": [ 29 | { 30 | "scale": [ 31 | 4.0, 32 | 2.0, 33 | 0.5, 34 | 0.5 35 | ], 36 | "type": "scale" 37 | } 38 | ], 39 | "path": "0" 40 | }, 41 | { 42 | "coordinateTransformations": [ 43 | { 44 | "scale": [ 45 | 4.0, 46 | 2.0, 47 | 1.0, 48 | 1.0 49 | ], 50 | "type": "scale" 51 | } 52 | ], 53 | "path": "1" 54 | } 55 | ], 56 | "version": "0.4" 57 | } 58 | ], 59 | "omero": { 60 | "channels": [ 61 | { 62 | "active": true, 63 | "color": "00FFFF", 64 | "label": "channel_0", 65 | "wavelength_id": "channel_0", 66 | "window": { 67 | "end": 255.0, 68 | "max": 255.0, 69 | "min": 0.0, 70 | "start": 0.0 71 | } 72 | } 73 | ] 74 | } 75 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 3, 5 | 64, 6 | 64 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 4, 22 | 3, 23 | 64, 24 | 64 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 3, 5 | 32, 6 | 32 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 4, 22 | 3, 23 | 32, 24 | 32 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "t", 13 | "type": "time", 14 | "unit": "seconds" 15 | }, 16 | { 17 | "name": "z", 18 | "type": "space", 19 | "unit": "micrometer" 20 | }, 21 | { 22 | "name": "y", 23 | "type": "space", 24 | "unit": "micrometer" 25 | }, 26 | { 27 | "name": "x", 28 | "type": "space", 29 | "unit": "micrometer" 30 | } 31 | ], 32 | "datasets": [ 33 | { 34 | "coordinateTransformations": [ 35 | { 36 | "scale": [ 37 | 4.0, 38 | 2.0, 39 | 0.5, 40 | 0.5 41 | ], 42 | "type": "scale" 43 | } 44 | ], 45 | "path": "0" 46 | }, 47 | { 48 | "coordinateTransformations": [ 49 | { 50 | "scale": [ 51 | 4.0, 52 | 2.0, 53 | 1.0, 54 | 1.0 55 | ], 56 | "type": "scale" 57 | } 58 | ], 59 | "path": "1" 60 | } 61 | ], 62 | "name": "label", 63 | "version": "0.4" 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 3, 5 | 64, 6 | 64 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 4, 22 | 3, 23 | 64, 24 | 64 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_tzyx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 4, 4 | 3, 5 | 32, 6 | 32 7 | ], 8 | "compressor": { 9 | "blocksize": 0, 10 | "clevel": 5, 11 | "cname": "lz4", 12 | "id": "blosc", 13 | "shuffle": 1 14 | }, 15 | "dimension_separator": "/", 16 | "dtype": "|u1", 17 | "fill_value": 0, 18 | "filters": null, 19 | "order": "C", 20 | "shape": [ 21 | 4, 22 | 3, 23 | 32, 24 | 32 25 | ], 26 | "zarr_format": 2 27 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "y", 7 | "type": "space", 8 | "unit": "micrometer" 9 | }, 10 | { 11 | "name": "x", 12 | "type": "space", 13 | "unit": "micrometer" 14 | } 15 | ], 16 | "datasets": [ 17 | { 18 | "coordinateTransformations": [ 19 | { 20 | "scale": [ 21 | 0.5, 22 | 0.5 23 | ], 24 | "type": "scale" 25 | } 26 | ], 27 | "path": "0" 28 | }, 29 | { 30 | "coordinateTransformations": [ 31 | { 32 | "scale": [ 33 | 1.0, 34 | 1.0 35 | ], 36 | "type": "scale" 37 | } 38 | ], 39 | "path": "1" 40 | } 41 | ], 42 | "version": "0.4" 43 | } 44 | ], 45 | "omero": { 46 | "channels": [ 47 | { 48 | "active": true, 49 | "color": "00FFFF", 50 | "label": "channel_0", 51 | "wavelength_id": "channel_0", 52 | "window": { 53 | "end": 255.0, 54 | "max": 255.0, 55 | "min": 0.0, 56 | "start": 0.0 57 | } 58 | } 59 | ] 60 | } 61 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 64, 4 | 64 5 | ], 6 | "compressor": { 7 | "blocksize": 0, 8 | "clevel": 5, 9 | "cname": "lz4", 10 | "id": "blosc", 11 | "shuffle": 1 12 | }, 13 | "dimension_separator": "/", 14 | "dtype": "|u1", 15 | "fill_value": 0, 16 | "filters": null, 17 | "order": "C", 18 | "shape": [ 19 | 64, 20 | 64 21 | ], 22 | "zarr_format": 2 23 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 32, 4 | 32 5 | ], 6 | "compressor": { 7 | "blocksize": 0, 8 | "clevel": 5, 9 | "cname": "lz4", 10 | "id": "blosc", 11 | "shuffle": 1 12 | }, 13 | "dimension_separator": "/", 14 | "dtype": "|u1", 15 | "fill_value": 0, 16 | "filters": null, 17 | "order": "C", 18 | "shape": [ 19 | 32, 20 | 32 21 | ], 22 | "zarr_format": 2 23 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "y", 13 | "type": "space", 14 | "unit": "micrometer" 15 | }, 16 | { 17 | "name": "x", 18 | "type": "space", 19 | "unit": "micrometer" 20 | } 21 | ], 22 | "datasets": [ 23 | { 24 | "coordinateTransformations": [ 25 | { 26 | "scale": [ 27 | 0.5, 28 | 0.5 29 | ], 30 | "type": "scale" 31 | } 32 | ], 33 | "path": "0" 34 | }, 35 | { 36 | "coordinateTransformations": [ 37 | { 38 | "scale": [ 39 | 1.0, 40 | 1.0 41 | ], 42 | "type": "scale" 43 | } 44 | ], 45 | "path": "1" 46 | } 47 | ], 48 | "name": "label", 49 | "version": "0.4" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 64, 4 | 64 5 | ], 6 | "compressor": { 7 | "blocksize": 0, 8 | "clevel": 5, 9 | "cname": "lz4", 10 | "id": "blosc", 11 | "shuffle": 1 12 | }, 13 | "dimension_separator": "/", 14 | "dtype": "|u1", 15 | "fill_value": 0, 16 | "filters": null, 17 | "order": "C", 18 | "shape": [ 19 | 64, 20 | 64 21 | ], 22 | "zarr_format": 2 23 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_yx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 32, 4 | 32 5 | ], 6 | "compressor": { 7 | "blocksize": 0, 8 | "clevel": 5, 9 | "cname": "lz4", 10 | "id": "blosc", 11 | "shuffle": 1 12 | }, 13 | "dimension_separator": "/", 14 | "dtype": "|u1", 15 | "fill_value": 0, 16 | "filters": null, 17 | "order": "C", 18 | "shape": [ 19 | 32, 20 | 32 21 | ], 22 | "zarr_format": 2 23 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | { 6 | "name": "z", 7 | "type": "space", 8 | "unit": "micrometer" 9 | }, 10 | { 11 | "name": "y", 12 | "type": "space", 13 | "unit": "micrometer" 14 | }, 15 | { 16 | "name": "x", 17 | "type": "space", 18 | "unit": "micrometer" 19 | } 20 | ], 21 | "datasets": [ 22 | { 23 | "coordinateTransformations": [ 24 | { 25 | "scale": [ 26 | 2.0, 27 | 0.5, 28 | 0.5 29 | ], 30 | "type": "scale" 31 | } 32 | ], 33 | "path": "0" 34 | }, 35 | { 36 | "coordinateTransformations": [ 37 | { 38 | "scale": [ 39 | 2.0, 40 | 1.0, 41 | 1.0 42 | ], 43 | "type": "scale" 44 | } 45 | ], 46 | "path": "1" 47 | } 48 | ], 49 | "version": "0.4" 50 | } 51 | ], 52 | "omero": { 53 | "channels": [ 54 | { 55 | "active": true, 56 | "color": "00FFFF", 57 | "label": "channel_0", 58 | "wavelength_id": "channel_0", 59 | "window": { 60 | "end": 255.0, 61 | "max": 255.0, 62 | "min": 0.0, 63 | "start": 0.0 64 | } 65 | } 66 | ] 67 | } 68 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 3, 4 | 64, 5 | 64 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 3, 21 | 64, 22 | 64 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 3, 4 | 32, 5 | 32 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 3, 21 | 32, 22 | 32 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/labels/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "labels": [ 3 | "label" 4 | ] 5 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/labels/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/labels/label/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | { 12 | "name": "z", 13 | "type": "space", 14 | "unit": "micrometer" 15 | }, 16 | { 17 | "name": "y", 18 | "type": "space", 19 | "unit": "micrometer" 20 | }, 21 | { 22 | "name": "x", 23 | "type": "space", 24 | "unit": "micrometer" 25 | } 26 | ], 27 | "datasets": [ 28 | { 29 | "coordinateTransformations": [ 30 | { 31 | "scale": [ 32 | 2.0, 33 | 0.5, 34 | 0.5 35 | ], 36 | "type": "scale" 37 | } 38 | ], 39 | "path": "0" 40 | }, 41 | { 42 | "coordinateTransformations": [ 43 | { 44 | "scale": [ 45 | 2.0, 46 | 1.0, 47 | 1.0 48 | ], 49 | "type": "scale" 50 | } 51 | ], 52 | "path": "1" 53 | } 54 | ], 55 | "name": "label", 56 | "version": "0.4" 57 | } 58 | ] 59 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/labels/label/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format": 2 3 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/labels/label/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 3, 4 | 64, 5 | 64 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 3, 21 | 64, 22 | 64 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/images/test_image_zyx.zarr/labels/label/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks": [ 3 | 3, 4 | 32, 5 | 32 6 | ], 7 | "compressor": { 8 | "blocksize": 0, 9 | "clevel": 5, 10 | "cname": "lz4", 11 | "id": "blosc", 12 | "shuffle": 1 13 | }, 14 | "dimension_separator": "/", 15 | "dtype": "|u1", 16 | "fill_value": 0, 17 | "filters": null, 18 | "order": "C", 19 | "shape": [ 20 | 3, 21 | 32, 22 | 32 23 | ], 24 | "zarr_format": 2 25 | } -------------------------------------------------------------------------------- /tests/data/v04/meta/base_ome_zarr_image_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | {"name": "c", "type": "channel"}, 6 | {"name": "z", "type": "space", "unit": "micrometer"}, 7 | {"name": "y", "type": "space", "unit": "micrometer"}, 8 | {"name": "x", "type": "space", "unit": "micrometer"} 9 | ], 10 | "datasets": [ 11 | { 12 | "coordinateTransformations": [ 13 | {"scale": [1.0, 1.0, 0.1625, 0.1625], "type": "scale"} 14 | ], 15 | "path": "0" 16 | }, 17 | { 18 | "coordinateTransformations": [ 19 | {"scale": [1.0, 1.0, 0.325, 0.325], "type": "scale"} 20 | ], 21 | "path": "1" 22 | }, 23 | { 24 | "coordinateTransformations": [ 25 | {"scale": [1.0, 1.0, 0.65, 0.65], "type": "scale"} 26 | ], 27 | "path": "2" 28 | }, 29 | { 30 | "coordinateTransformations": [ 31 | {"scale": [1.0, 1.0, 1.3, 1.3], "type": "scale"} 32 | ], 33 | "path": "3" 34 | }, 35 | { 36 | "coordinateTransformations": [ 37 | {"scale": [1.0, 1.0, 2.6, 2.6], "type": "scale"} 38 | ], 39 | "path": "4" 40 | } 41 | ], 42 | "version": "0.4" 43 | } 44 | ], 45 | "omero": { 46 | "channels": [ 47 | { 48 | "active": true, 49 | "color": "00FFFF", 50 | "label": "DAPI", 51 | "wavelength_id": "A01_C01", 52 | "window": {"end": 700, "max": 65535, "min": 0, "start": 0} 53 | }, 54 | { 55 | "active": true, 56 | "color": "FF00FF", 57 | "label": "nanog", 58 | "wavelength_id": "A01_C02", 59 | "window": {"end": 180, "max": 65535, "min": 0, "start": 0} 60 | }, 61 | { 62 | "active": true, 63 | "color": "FFFF00", 64 | "label": "Lamin B1", 65 | "wavelength_id": "A02_C03", 66 | "window": {"end": 1500, "max": 65535, "min": 0, "start": 0} 67 | } 68 | ], 69 | "id": 1, 70 | "name": "TBD", 71 | "version": "0.4" 72 | } 73 | } -------------------------------------------------------------------------------- /tests/data/v04/meta/base_ome_zarr_image_meta_wrong_axis_order.json: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales": [ 3 | { 4 | "axes": [ 5 | {"name": "z", "type": "space", "unit": "micrometer"}, 6 | {"name": "c", "type": "channel"}, 7 | {"name": "y", "type": "space", "unit": "micrometer"}, 8 | {"name": "x", "type": "space", "unit": "micrometer"} 9 | ], 10 | "datasets": [ 11 | { 12 | "coordinateTransformations": [ 13 | {"scale": [1.0, 1.0, 0.1625, 0.1625], "type": "scale"} 14 | ], 15 | "path": "0" 16 | }, 17 | { 18 | "coordinateTransformations": [ 19 | {"scale": [1.0, 1.0, 0.325, 0.325], "type": "scale"} 20 | ], 21 | "path": "1" 22 | }, 23 | { 24 | "coordinateTransformations": [ 25 | {"scale": [1.0, 1.0, 0.65, 0.65], "type": "scale"} 26 | ], 27 | "path": "2" 28 | }, 29 | { 30 | "coordinateTransformations": [ 31 | {"scale": [1.0, 1.0, 1.3, 1.3], "type": "scale"} 32 | ], 33 | "path": "3" 34 | }, 35 | { 36 | "coordinateTransformations": [ 37 | {"scale": [1.0, 1.0, 2.6, 2.6], "type": "scale"} 38 | ], 39 | "path": "4" 40 | } 41 | ], 42 | "version": "0.4" 43 | } 44 | ], 45 | "omero": { 46 | "channels": [ 47 | { 48 | "active": true, 49 | "color": "00FFFF", 50 | "label": "DAPI", 51 | "wavelength_id": "A01_C01", 52 | "window": {"end": 700, "max": 65535, "min": 0, "start": 0} 53 | }, 54 | { 55 | "active": true, 56 | "color": "FF00FF", 57 | "label": "nanog", 58 | "wavelength_id": "A01_C02", 59 | "window": {"end": 180, "max": 65535, "min": 0, "start": 0} 60 | }, 61 | { 62 | "active": true, 63 | "color": "FFFF00", 64 | "label": "Lamin B1", 65 | "wavelength_id": "A02_C03", 66 | "window": {"end": 1500, "max": 65535, "min": 0, "start": 0} 67 | } 68 | ], 69 | "id": 1, 70 | "name": "TBD", 71 | "version": "0.4" 72 | } 73 | } -------------------------------------------------------------------------------- /tests/data/v04/meta/base_ome_zarr_label_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "image-label": { 3 | "source": { 4 | "image": "../../" 5 | }, 6 | "version": "0.4" 7 | }, 8 | "multiscales": [ 9 | { 10 | "axes": [ 11 | {"name": "z", "type": "space", "unit": "micrometer"}, 12 | {"name": "y", "type": "space", "unit": "micrometer"}, 13 | {"name": "x", "type": "space", "unit": "micrometer"} 14 | ], 15 | "datasets": [ 16 | { 17 | "coordinateTransformations": [ 18 | {"scale": [1.0, 0.1625, 0.1625], "type": "scale"} 19 | ], 20 | "path": "0" 21 | }, 22 | { 23 | "coordinateTransformations": [ 24 | {"scale": [1.0, 0.325, 0.325], "type": "scale"} 25 | ], 26 | "path": "1" 27 | }, 28 | { 29 | "coordinateTransformations": [ 30 | {"scale": [1.0, 0.65, 0.65], "type": "scale"} 31 | ], 32 | "path": "2" 33 | }, 34 | { 35 | "coordinateTransformations": [ 36 | {"scale": [1.0, 1.3, 1.3], "type": "scale"} 37 | ], 38 | "path": "3" 39 | }, 40 | { 41 | "coordinateTransformations": [ 42 | {"scale": [1.0, 2.6, 2.6], "type": "scale"} 43 | ], 44 | "path": "4" 45 | } 46 | ], 47 | "version": "0.4" 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /tests/data/v04/meta/base_ome_zarr_well_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "well": { 3 | "images": [ 4 | { 5 | "path": "0" 6 | } 7 | ], 8 | "version": "0.4" 9 | } 10 | } -------------------------------------------------------------------------------- /tests/data/v04/meta/ome_zarr_well_path_normalization_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "well": { 3 | "images": [ 4 | { 5 | "path": "0" 6 | }, 7 | { 8 | "path": "0_mip" 9 | } 10 | ], 11 | "version": "0.4" 12 | } 13 | } -------------------------------------------------------------------------------- /tests/unit/common/test_dimensions.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ngio.common import Dimensions 4 | from ngio.ome_zarr_meta import AxesMapper 5 | from ngio.ome_zarr_meta.ngio_specs import Axis 6 | from ngio.utils import NgioValidationError, NgioValueError 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "axes_names", 11 | [ 12 | ["x", "y", "z", "c"], 13 | ["x", "y", "c"], 14 | ["z", "c", "x", "y"], 15 | ["t", "z", "c", "x", "y"], 16 | ["x", "y", "z", "t"], 17 | ], 18 | ) 19 | def test_dimensions(axes_names): 20 | axes = [Axis(on_disk_name=name) for name in axes_names] 21 | canonic_dim_dict = dict(zip("tczyx", (2, 3, 4, 5, 6), strict=True)) 22 | dim_dict = {ax: canonic_dim_dict.get(ax, 1) for ax in axes_names} 23 | 24 | shape = tuple(dim_dict.get(ax) for ax in axes_names) 25 | shape = tuple(s for s in shape if s is not None) 26 | 27 | ax_mapper = AxesMapper(on_disk_axes=axes) 28 | dims = Dimensions(shape=shape, axes_mapper=ax_mapper) 29 | 30 | assert isinstance(dims.__repr__(), str) 31 | 32 | for ax, s in dim_dict.items(): 33 | assert dims.get(ax) == s 34 | 35 | if dim_dict.get("z", 1) > 1: 36 | assert dims.is_3d 37 | 38 | if dim_dict.get("c", 1) > 1: 39 | assert dims.is_multi_channels 40 | 41 | if dim_dict.get("t", 1) > 1: 42 | assert dims.is_time_series 43 | 44 | if dim_dict.get("z", 1) > 1 and dim_dict.get("t", 1) > 1: 45 | assert dims.is_3d_time_series 46 | 47 | if dim_dict.get("z", 1) == 1 and dim_dict.get("t", 1) == 1: 48 | assert dims.is_2d 49 | 50 | if dim_dict.get("z", 1) == 1 and dim_dict.get("t", 1) > 1: 51 | assert dims.is_2d_time_series 52 | 53 | assert dims.get_canonical_shape() == tuple(dim_dict.get(ax, 1) for ax in "tczyx") 54 | 55 | assert dims.on_disk_shape == shape 56 | 57 | 58 | def test_dimensions_error(): 59 | axes = [Axis(on_disk_name="x"), Axis(on_disk_name="y")] 60 | shape = (1, 2, 3) 61 | 62 | with pytest.raises(NgioValidationError): 63 | Dimensions(shape=shape, axes_mapper=AxesMapper(on_disk_axes=axes)) 64 | 65 | shape = (3, 4) 66 | dims = Dimensions(shape=shape, axes_mapper=AxesMapper(on_disk_axes=axes)) 67 | 68 | assert dims.get_shape(axes_order=["c", "x", "y", "z"]) == (1, 3, 4, 1) 69 | assert dims.get_shape(axes_order=["c", "z", "y", "x"]) == (1, 1, 4, 3) 70 | 71 | with pytest.raises(NgioValueError): 72 | dims.get("c", strict=True) 73 | 74 | assert not dims.is_3d 75 | assert not dims.is_multi_channels 76 | assert not dims.is_time_series 77 | assert not dims.is_3d_time_series 78 | assert not dims.is_2d_time_series 79 | -------------------------------------------------------------------------------- /tests/unit/common/test_pyramid.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | import zarr 5 | 6 | from ngio.common._pyramid import on_disk_zoom 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "order, mode", 11 | [ 12 | (0, "dask"), 13 | (1, "dask"), 14 | (0, "numpy"), 15 | (1, "numpy"), 16 | (0, "coarsen"), 17 | (1, "coarsen"), 18 | ], 19 | ) 20 | def test_on_disk_zooms(tmp_path: Path, order: int, mode: str): 21 | source = tmp_path / "source.zarr" 22 | source_array = zarr.open_array(source, shape=(16, 128, 128), dtype="uint8") 23 | 24 | target = tmp_path / "target.zarr" 25 | target_array = zarr.open_array(target, shape=(16, 64, 64), dtype="uint8") 26 | 27 | on_disk_zoom(source_array, target_array, order=order, mode=mode) 28 | -------------------------------------------------------------------------------- /tests/unit/common/test_roi.py: -------------------------------------------------------------------------------- 1 | from ngio import PixelSize 2 | from ngio.common import Dimensions, Roi 3 | from ngio.ome_zarr_meta.ngio_specs import AxesMapper, Axis 4 | 5 | 6 | def test_rois(): 7 | roi = Roi( 8 | name="test", 9 | x=0.0, 10 | y=0.0, 11 | z=0.0, 12 | x_length=1.0, 13 | y_length=1.0, 14 | z_length=1.0, 15 | unit="micrometer", # type: ignore 16 | other="other", # type: ignore 17 | ) 18 | 19 | assert roi.x == 0.0 20 | 21 | axes = [Axis(on_disk_name="x"), Axis(on_disk_name="y")] 22 | ax_mapper = AxesMapper(on_disk_axes=axes) 23 | dims = Dimensions(shape=(30, 30), axes_mapper=ax_mapper) 24 | 25 | pixel_size = PixelSize(x=1.0, y=1.0, z=1.0) 26 | raster_roi = roi.to_pixel_roi(pixel_size, dims) 27 | 28 | assert raster_roi.to_slices() == { 29 | "x": slice(0, 1), 30 | "y": slice(0, 1), 31 | "z": slice(0, 1), 32 | } 33 | assert roi.model_extra is not None 34 | assert roi.model_extra["other"] == "other" 35 | 36 | world_roi_2 = raster_roi.to_roi(pixel_size) 37 | 38 | assert world_roi_2.x == 0.0 39 | assert world_roi_2.y == 0.0 40 | assert world_roi_2.z == 0.0 41 | assert world_roi_2.x_length == 1.0 42 | assert world_roi_2.y_length == 1.0 43 | assert world_roi_2.z_length == 1.0 44 | -------------------------------------------------------------------------------- /tests/unit/hcs/test_well.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | import zarr 5 | 6 | from ngio import create_empty_well, open_ome_zarr_well 7 | from ngio.utils import NgioValueError 8 | 9 | 10 | def test_open_real_ome_zarr_well(cardiomyocyte_tiny_path: Path): 11 | cardiomyocyte_tiny_path = cardiomyocyte_tiny_path 12 | cardiomyocyte_tiny_path = cardiomyocyte_tiny_path / "B" / "03" 13 | ome_zarr_well = open_ome_zarr_well(cardiomyocyte_tiny_path) 14 | assert isinstance(ome_zarr_well.__repr__(), str) 15 | assert ome_zarr_well.paths() == ["0"] 16 | assert ome_zarr_well.acquisition_ids == [] 17 | 18 | 19 | def test_create_and_edit_well(tmp_path: Path): 20 | test_well = create_empty_well(tmp_path / "test_well.zarr") 21 | assert test_well.paths() == [] 22 | assert test_well.acquisition_ids == [] 23 | 24 | test_well.add_image(image_path="0", acquisition_id=0, strict=False) 25 | test_well.add_image(image_path="1", acquisition_id=0, strict=True) 26 | 27 | with pytest.raises(NgioValueError): 28 | test_well.add_image(image_path="1", acquisition_id=1) 29 | 30 | test_well.atomic_add_image(image_path="2", acquisition_id=1, strict=False) 31 | assert len(test_well.paths()) == 3 32 | assert test_well.acquisition_ids == [0, 1], test_well.acquisition_ids 33 | 34 | store = test_well.get_image_store("0") 35 | assert isinstance(store, zarr.Group) 36 | -------------------------------------------------------------------------------- /tests/unit/images/test_create.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from ngio import create_empty_ome_zarr, create_ome_zarr_from_array 7 | from ngio.utils import NgioValueError 8 | 9 | 10 | @pytest.mark.parametrize( 11 | "create_kwargs", 12 | [ 13 | { 14 | "store": "test_image_yx._zarr", 15 | "shape": (64, 64), 16 | "xy_pixelsize": 0.5, 17 | "axes_names": ["y", "x"], 18 | }, 19 | { 20 | "store": "test_image_cyx.zarr", 21 | "shape": (2, 64, 64), 22 | "xy_pixelsize": 0.5, 23 | "axes_names": ["c", "y", "x"], 24 | "channel_labels": ["channel1", "channel2"], 25 | }, 26 | { 27 | "store": "test_image_zyx.zarr", 28 | "shape": (3, 64, 64), 29 | "xy_pixelsize": 0.5, 30 | "z_spacing": 2.0, 31 | "axes_names": ["z", "y", "x"], 32 | }, 33 | { 34 | "store": "test_image_czyx.zarr", 35 | "shape": (2, 3, 64, 64), 36 | "xy_pixelsize": 0.5, 37 | "z_spacing": 2.0, 38 | "axes_names": ["c", "z", "y", "x"], 39 | "channel_labels": ["channel1", "channel2"], 40 | }, 41 | { 42 | "store": "test_image_c1yx.zarr", 43 | "shape": (2, 1, 64, 64), 44 | "xy_pixelsize": 0.5, 45 | "z_spacing": 1.0, 46 | "axes_names": ["c", "z", "y", "x"], 47 | "channel_labels": ["channel1", "channel2"], 48 | }, 49 | { 50 | "store": "test_image_tyx.zarr", 51 | "shape": (4, 64, 64), 52 | "xy_pixelsize": 0.5, 53 | "time_spacing": 4.0, 54 | "axes_names": ["t", "y", "x"], 55 | }, 56 | { 57 | "store": "test_image_tcyx.zarr", 58 | "shape": (4, 2, 64, 64), 59 | "xy_pixelsize": 0.5, 60 | "time_spacing": 4.0, 61 | "axes_names": ["t", "c", "y", "x"], 62 | "channel_labels": ["channel1", "channel2"], 63 | }, 64 | { 65 | "store": "test_image_tzyx.zarr", 66 | "shape": (4, 3, 64, 64), 67 | "xy_pixelsize": 0.5, 68 | "z_spacing": 2.0, 69 | "time_spacing": 4.0, 70 | "axes_names": ["t", "z", "y", "x"], 71 | }, 72 | { 73 | "store": "test_image_tczyx.zarr", 74 | "shape": (4, 2, 3, 64, 64), 75 | "xy_pixelsize": 0.5, 76 | "z_spacing": 2.0, 77 | "time_spacing": 4.0, 78 | "axes_names": ["t", "c", "z", "y", "x"], 79 | "channel_labels": ["channel1", "channel2"], 80 | }, 81 | ], 82 | ) 83 | def test_create_empty(tmp_path: Path, create_kwargs: dict): 84 | create_kwargs["store"] = tmp_path / create_kwargs["store"] 85 | ome_zarr = create_empty_ome_zarr(**create_kwargs, dtype="uint8", levels=1) 86 | ome_zarr.derive_label("label1") 87 | 88 | shape = create_kwargs.pop("shape") 89 | array = np.random.randint(0, 255, shape, dtype="uint8") 90 | create_ome_zarr_from_array(array=array, **create_kwargs, levels=1, overwrite=True) 91 | 92 | 93 | def test_create_fail(tmp_path: Path): 94 | with pytest.raises(NgioValueError): 95 | create_ome_zarr_from_array( 96 | array=np.random.randint(0, 255, (64, 64), dtype="uint8"), 97 | store=tmp_path / "fail.zarr", 98 | xy_pixelsize=0.5, 99 | axes_names=["z", "y", "x"], # should fail expected yx 100 | levels=1, 101 | overwrite=True, 102 | ) 103 | 104 | with pytest.raises(NgioValueError): 105 | create_ome_zarr_from_array( 106 | array=np.random.randint(0, 255, (2, 64, 64), dtype="uint8"), 107 | store=tmp_path / "fail.zarr", 108 | xy_pixelsize=0.5, 109 | axes_names=["c", "y", "x"], 110 | levels=1, 111 | channel_labels=[ 112 | "channel1", 113 | "channel2", 114 | "channel3", 115 | ], # should fail expected 2 channels 116 | overwrite=True, 117 | ) 118 | 119 | with pytest.raises(NgioValueError): 120 | create_ome_zarr_from_array( 121 | array=np.random.randint(0, 255, (2, 64, 64), dtype="uint8"), 122 | store=tmp_path / "fail.zarr", 123 | xy_pixelsize=0.5, 124 | axes_names=["c", "y", "x"], 125 | levels=1, 126 | chunks=(1, 64, 64, 64), # should fail expected 3 axes 127 | overwrite=True, 128 | ) 129 | -------------------------------------------------------------------------------- /tests/unit/images/test_images.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from ngio import Image, open_image 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "zarr_name", 10 | [ 11 | "test_image_yx.zarr", 12 | "test_image_cyx.zarr", 13 | "test_image_zyx.zarr", 14 | "test_image_czyx.zarr", 15 | "test_image_c1yx.zarr", 16 | "test_image_tyx.zarr", 17 | "test_image_tcyx.zarr", 18 | "test_image_tzyx.zarr", 19 | "test_image_tczyx.zarr", 20 | ], 21 | ) 22 | def test_open_image(images_v04: dict[str, Path], zarr_name: str): 23 | path = images_v04[zarr_name] 24 | image = open_image(path) 25 | assert isinstance(image, Image) 26 | -------------------------------------------------------------------------------- /tests/unit/images/test_masked_images.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pytest 5 | from scipy import ndimage 6 | from skimage.segmentation import watershed 7 | 8 | from ngio import create_ome_zarr_from_array 9 | 10 | 11 | def _draw_random_labels(shape: tuple[int, ...], num_regions: int): 12 | np.random.seed(0) 13 | markers = np.zeros(shape, dtype=np.int32) 14 | seeds_list = np.random.randint(0, shape[0], size=(num_regions, 2)) 15 | for i, (y, x) in enumerate(seeds_list, start=1): 16 | markers[y, x] = i 17 | 18 | image = ndimage.distance_transform_edt(markers == 0).astype("uint16") 19 | labels = watershed(image, markers) 20 | return image, labels 21 | 22 | 23 | @pytest.mark.parametrize( 24 | "array_mode, shape", 25 | [("numpy", (64, 64)), ("dask", (64, 64)), ("numpy", (16, 32, 32))], 26 | ) 27 | def test_masking(tmp_path: Path, array_mode: str, shape: tuple[int, ...]): 28 | mask, label_image = _draw_random_labels(shape=shape, num_regions=20) 29 | unique_labels, counts = np.unique(label_image, return_counts=True) 30 | labels_stats = dict(zip(unique_labels, counts, strict=True)) 31 | 32 | store = tmp_path / "test_image_yx_random_label.zarr" 33 | # Create a new ome_zarr with the mask 34 | ome_zarr = create_ome_zarr_from_array( 35 | store=store, 36 | array=mask, 37 | xy_pixelsize=0.5, 38 | levels=1, 39 | overwrite=True, 40 | ) 41 | label = ome_zarr.derive_label("label") 42 | label.set_array(label_image) 43 | 44 | # Masking image test 45 | masked_image = ome_zarr.get_masked_image("label") 46 | assert isinstance(masked_image.__repr__(), str) 47 | _roi_array = masked_image.get_roi(label=1, zoom_factor=1.123, mode=array_mode) 48 | masked_image.set_roi_masked( 49 | label=1, patch=np.ones_like(_roi_array), zoom_factor=1.123 50 | ) 51 | 52 | _roi_mask = masked_image.get_roi_masked(label=1, mode=array_mode) 53 | # Check that the mask is binary after masking 54 | np.testing.assert_allclose(np.unique(_roi_mask), [0, 1]) 55 | 56 | # Just test the API 57 | masked_image.set_roi(label=1, patch=np.zeros_like(_roi_array), zoom_factor=1.123) 58 | 59 | # Masking label test (recreate the label) 60 | ome_zarr.derive_label("empty_label") 61 | masked_new_label = ome_zarr.get_masked_label( 62 | "empty_label", masking_label_name="label" 63 | ) 64 | assert isinstance(masked_new_label.__repr__(), str) 65 | 66 | for label_id in labels_stats.keys(): 67 | label_mask = masked_new_label.get_roi(label_id, mode=array_mode) 68 | label_mask = np.full(label_mask.shape, label_id, dtype=label_mask.dtype) 69 | # Set the label only inside the mask 70 | masked_new_label.set_roi_masked(label_id, label_mask) 71 | 72 | # rerun the stats on the new masked label 73 | unique_labels, counts = np.unique(masked_new_label.get_array(), return_counts=True) 74 | labels_stats_masked = dict(zip(unique_labels, counts, strict=True)) 75 | assert labels_stats == labels_stats_masked 76 | -------------------------------------------------------------------------------- /tests/unit/ome_zarr_meta/test_image_handler.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from ngio.ome_zarr_meta import NgioImageMeta, find_image_meta_handler 4 | from ngio.utils import ZarrGroupHandler 5 | 6 | 7 | def test_get_image_handler(cardiomyocyte_tiny_path: Path): 8 | # TODO this is a placeholder test 9 | # The pooch cache is giving us trouble here 10 | cardiomyocyte_tiny_path = cardiomyocyte_tiny_path / "B" / "03" / "0" 11 | group_handler = ZarrGroupHandler(cardiomyocyte_tiny_path) 12 | handler = find_image_meta_handler(group_handler) 13 | meta = handler.safe_load_meta() 14 | assert isinstance(meta, NgioImageMeta) 15 | handler.write_meta(meta) 16 | -------------------------------------------------------------------------------- /tests/unit/ome_zarr_meta/test_unit_v04_utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from ome_zarr_models.v04.image import ImageAttrs as ImageAttrsV04 4 | from ome_zarr_models.v04.image_label import ImageLabelAttrs as LabelAttrsV04 5 | from ome_zarr_models.v04.well import WellAttrs as WellAttrsV04 6 | 7 | from ngio.ome_zarr_meta import NgioImageMeta, NgioLabelMeta, NgioWellMeta 8 | from ngio.ome_zarr_meta.v04._v04_spec_utils import ( 9 | _is_v04_image_meta, 10 | _is_v04_label_meta, 11 | ngio_to_v04_image_meta, 12 | ngio_to_v04_label_meta, 13 | ngio_to_v04_well_meta, 14 | v04_to_ngio_image_meta, 15 | v04_to_ngio_label_meta, 16 | v04_to_ngio_well_meta, 17 | ) 18 | 19 | 20 | def test_image_round_trip(): 21 | path = "tests/data/v04/meta/base_ome_zarr_image_meta.json" 22 | with open(path) as f: 23 | input_metadata = json.load(f) 24 | 25 | assert _is_v04_image_meta(input_metadata) 26 | is_valid, ngio_image = v04_to_ngio_image_meta(input_metadata) 27 | assert is_valid 28 | assert isinstance(ngio_image, NgioImageMeta) 29 | output_metadata = ngio_to_v04_image_meta(ngio_image) 30 | assert ImageAttrsV04(**output_metadata) == ImageAttrsV04(**input_metadata) 31 | 32 | 33 | def test_label_round_trip(): 34 | path = "tests/data/v04/meta/base_ome_zarr_label_meta.json" 35 | with open(path) as f: 36 | metadata = json.load(f) 37 | 38 | assert _is_v04_label_meta(metadata) 39 | 40 | is_valid, ngio_label = v04_to_ngio_label_meta(metadata) 41 | assert is_valid 42 | assert isinstance(ngio_label, NgioLabelMeta) 43 | output_metadata = ngio_to_v04_label_meta(ngio_label) 44 | assert LabelAttrsV04(**output_metadata) == LabelAttrsV04(**metadata) 45 | 46 | 47 | def test_well_meta(): 48 | path = "tests/data/v04/meta/base_ome_zarr_well_meta.json" 49 | with open(path) as f: 50 | metadata = json.load(f) 51 | 52 | is_valid, ngio_well = v04_to_ngio_well_meta(metadata) 53 | assert is_valid 54 | assert isinstance(ngio_well, NgioWellMeta) 55 | output_metadata = ngio_to_v04_well_meta(ngio_well) 56 | assert isinstance(output_metadata, dict) 57 | assert WellAttrsV04(**output_metadata) == WellAttrsV04(**metadata) 58 | 59 | 60 | def test_well_meta_path_normalization(): 61 | path = "tests/data/v04/meta/ome_zarr_well_path_normalization_meta.json" 62 | with open(path) as f: 63 | metadata = json.load(f) 64 | 65 | is_valid, ngio_well = v04_to_ngio_well_meta(metadata) 66 | assert is_valid 67 | assert isinstance(ngio_well, NgioWellMeta) 68 | output_metadata = ngio_to_v04_well_meta(ngio_well) 69 | assert isinstance(output_metadata, dict) 70 | 71 | images = [image["path"] for image in output_metadata["well"]["images"]] 72 | assert images == ["0", "0mip"] 73 | -------------------------------------------------------------------------------- /tests/unit/tables/test_feature_table.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pandas as pd 4 | import pytest 5 | 6 | from ngio.tables.tables_container import open_table, write_table 7 | from ngio.tables.v1 import FeatureTableV1 8 | 9 | 10 | @pytest.mark.parametrize("backend", ["experimental_json_v1", "anndata_v1"]) 11 | def test_generic_table(tmp_path: Path, backend: str): 12 | store = tmp_path / "test.zarr" 13 | test_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "label": [1, 2, 3]}) 14 | table = FeatureTableV1(test_df, reference_label="label") 15 | assert isinstance(table.__repr__(), str) 16 | assert table._meta.region.path == "../labels/label" 17 | assert table.reference_label == "label" 18 | 19 | write_table(store=store, table=table, backend=backend) 20 | 21 | loaded_table = open_table(store=store) 22 | assert isinstance(loaded_table, FeatureTableV1) 23 | assert set(loaded_table.dataframe.columns) == {"a", "b"} 24 | for column in loaded_table.dataframe.columns: 25 | pd.testing.assert_series_equal( 26 | loaded_table.dataframe[column], test_df[column], check_index=False 27 | ) 28 | -------------------------------------------------------------------------------- /tests/unit/tables/test_generic_table.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import pytest 6 | from anndata import AnnData 7 | 8 | from ngio.tables.tables_container import open_table, write_table 9 | from ngio.tables.v1 import GenericTable 10 | 11 | 12 | @pytest.mark.parametrize("backend", ["experimental_json_v1", "anndata_v1"]) 13 | def test_generic_df_table(tmp_path: Path, backend: str): 14 | store = tmp_path / "test.zarr" 15 | test_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) 16 | table = GenericTable(dataframe=test_df) 17 | assert isinstance(table.__repr__(), str) 18 | 19 | write_table(store=store, table=table, backend=backend) 20 | 21 | loaded_table = open_table(store=store) 22 | assert isinstance(loaded_table, GenericTable) 23 | assert set(loaded_table.dataframe.columns) == {"a", "b"} 24 | for column in loaded_table.dataframe.columns: 25 | pd.testing.assert_series_equal( 26 | loaded_table.dataframe[column], test_df[column], check_index=False 27 | ) 28 | 29 | 30 | @pytest.mark.parametrize("backend", ["anndata_v1"]) 31 | def test_generic_anndata_table(tmp_path: Path, backend: str): 32 | store = tmp_path / "test.zarr" 33 | test_df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) 34 | test_obs = pd.DataFrame({"c": ["a", "b", "c"]}) 35 | test_obs.index = test_obs.index.astype(str) 36 | test_obsm = np.random.normal(0, 1, size=(3, 2)) 37 | 38 | anndata = AnnData(X=test_df, obs=test_obs) 39 | anndata.obsm["test"] = test_obsm 40 | 41 | table = GenericTable(anndata=anndata) 42 | 43 | table.dataframe = test_df 44 | assert not table.anndata_native 45 | table.anndata = anndata 46 | assert table.anndata_native 47 | 48 | write_table(store=store, table=table, backend=backend) 49 | 50 | loaded_table = open_table(store=store) 51 | assert isinstance(loaded_table, GenericTable) 52 | 53 | loaded_ad = loaded_table.anndata 54 | loaded_df = loaded_table.dataframe 55 | assert set(loaded_df.columns) == {"a", "b", "c"} 56 | 57 | np.testing.assert_allclose(loaded_ad.obsm["test"], test_obsm) # type: ignore 58 | -------------------------------------------------------------------------------- /tests/unit/tables/test_masking_roi_table_v1.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from ngio.tables.tables_container import open_table, write_table 6 | from ngio.tables.v1._roi_table import MaskingRoiTableV1, Roi 7 | from ngio.utils import NgioValueError 8 | 9 | 10 | def test_masking_roi_table_v1(tmp_path: Path): 11 | rois = { 12 | 1: Roi( 13 | name="1", 14 | x=0.0, 15 | y=0.0, 16 | z=0.0, 17 | x_length=1.0, 18 | y_length=1.0, 19 | z_length=1.0, 20 | unit="micrometer", # type: ignore 21 | ) 22 | } 23 | 24 | table = MaskingRoiTableV1(rois=rois.values(), reference_label="label") 25 | assert isinstance(table.__repr__(), str) 26 | assert table.reference_label == "label" 27 | 28 | table.add( 29 | roi=Roi( 30 | name="2", 31 | x=0.0, 32 | y=0.0, 33 | z=0.0, 34 | x_length=1.0, 35 | y_length=1.0, 36 | z_length=1.0, 37 | unit="micrometer", # type: ignore 38 | ) 39 | ) 40 | 41 | with pytest.raises(NgioValueError): 42 | table.add( 43 | roi=Roi( 44 | name="2", 45 | x=0.0, 46 | y=0.0, 47 | z=0.0, 48 | x_length=1.0, 49 | y_length=1.0, 50 | z_length=1.0, 51 | unit="micrometer", # type: ignore 52 | ) 53 | ) 54 | 55 | write_table(store=tmp_path / "roi_table.zarr", table=table, backend="anndata_v1") 56 | 57 | loaded_table = open_table(store=tmp_path / "roi_table.zarr") 58 | assert isinstance(loaded_table, MaskingRoiTableV1) 59 | 60 | assert loaded_table._meta.backend == "anndata_v1" 61 | assert loaded_table._meta.fractal_table_version == loaded_table.version() 62 | assert loaded_table._meta.type == loaded_table.type() 63 | -------------------------------------------------------------------------------- /tests/unit/tables/test_roi_table_v1.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pandas as pd 4 | import pytest 5 | 6 | from ngio.tables import RoiTable 7 | from ngio.tables.backends._anndata_v1 import AnnDataBackend 8 | from ngio.tables.tables_container import open_table, write_table 9 | from ngio.tables.v1._roi_table import Roi, RoiTableV1, RoiTableV1Meta 10 | from ngio.utils import NgioValueError, ZarrGroupHandler 11 | 12 | 13 | def test_roi_table_v1(tmp_path: Path): 14 | rois = { 15 | "roi1": Roi( 16 | name="roi1", 17 | x=0.0, 18 | y=0.0, 19 | z=0.0, 20 | x_length=1.0, 21 | y_length=1.0, 22 | z_length=1.0, 23 | unit="micrometer", # type: ignore 24 | ) 25 | } 26 | 27 | table = RoiTableV1(rois=rois.values()) 28 | assert isinstance(table.__repr__(), str) 29 | 30 | table.add( 31 | roi=Roi( 32 | name="roi2", 33 | x=0.0, 34 | y=0.0, 35 | z=0.0, 36 | x_length=1.0, 37 | y_length=1.0, 38 | z_length=1.0, 39 | unit="micrometer", # type: ignore 40 | ) 41 | ) 42 | 43 | with pytest.raises(NgioValueError): 44 | # ROI name already exists 45 | table.add( 46 | roi=Roi( 47 | name="roi2", 48 | x=0.0, 49 | y=0.0, 50 | z=0.0, 51 | x_length=1.0, 52 | y_length=1.0, 53 | z_length=1.0, 54 | unit="micrometer", # type: ignore 55 | ) 56 | ) 57 | 58 | table.add( 59 | roi=Roi( 60 | name="roi2", 61 | x=0.0, 62 | y=0.0, 63 | z=0.0, 64 | x_length=1.0, 65 | y_length=1.0, 66 | z_length=1.0, 67 | unit="micrometer", # type: ignore 68 | ), 69 | overwrite=True, 70 | ) 71 | 72 | write_table(store=tmp_path / "roi_table.zarr", table=table, backend="anndata_v1") 73 | 74 | loaded_table = open_table(store=tmp_path / "roi_table.zarr") 75 | assert isinstance(loaded_table, RoiTableV1) 76 | 77 | assert len(loaded_table._rois) == 2 78 | assert loaded_table.get("roi1") == table.get("roi1") 79 | assert loaded_table.get("roi2") == table.get("roi2") 80 | 81 | with pytest.raises(NgioValueError): 82 | loaded_table.get("roi3") 83 | 84 | assert loaded_table._meta.backend == "anndata_v1" 85 | assert loaded_table._meta.fractal_table_version == loaded_table.version() 86 | assert loaded_table._meta.type == loaded_table.type() 87 | 88 | 89 | def test_roi_no_index(tmp_path: Path): 90 | """ngio needs to support reading a table without an index. for legacy reasons""" 91 | handler = ZarrGroupHandler(tmp_path / "roi_table.zarr") 92 | backend = AnnDataBackend( 93 | group_handler=handler, 94 | ) 95 | 96 | roi_table = pd.DataFrame( 97 | { 98 | "x_micrometer": [0.0, 1.0], 99 | "y_micrometer": [0.0, 1.0], 100 | "z_micrometer": [0.0, 1.0], 101 | "len_x_micrometer": [1.0, 1.0], 102 | "len_y_micrometer": [1.0, 1.0], 103 | "len_z_micrometer": [1.0, 1.0], 104 | } 105 | ) 106 | roi_table.index = pd.Index(["roi_1", "roi_2"]) 107 | 108 | backend.write( 109 | roi_table, 110 | metadata=RoiTableV1Meta().model_dump(exclude_none=True), 111 | ) 112 | 113 | roi_table = RoiTable._from_handler(handler=handler) 114 | assert isinstance(roi_table, RoiTable) 115 | assert len(roi_table.rois()) == 2 116 | -------------------------------------------------------------------------------- /tests/unit/tables/test_table_group.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from pandas import DataFrame 5 | 6 | from ngio.tables.tables_container import ( 7 | FeatureTable, 8 | TablesContainer, 9 | open_tables_container, 10 | ) 11 | from ngio.utils import NgioValueError 12 | 13 | 14 | def test_table_container(tmp_path: Path): 15 | table_group = open_tables_container(tmp_path / "test.zarr") 16 | assert isinstance(table_group, TablesContainer) 17 | assert table_group.list() == [] 18 | 19 | # Create a feature table 20 | table = FeatureTable( 21 | dataframe=DataFrame({"label": [1, 2, 3], "a": [1.0, 1.3, 0.0]}) 22 | ) 23 | table_group.add(name="feat_table", table=table) 24 | assert table_group.list() == ["feat_table"] 25 | 26 | with pytest.raises(NgioValueError): 27 | table_group.add(name="feat_table", table=table) 28 | 29 | table = table_group.get("feat_table") 30 | assert isinstance(table, FeatureTable) 31 | 32 | expected = DataFrame({"label": [1, 2, 3], "a": [1.0, 1.3, 0.0]}) 33 | expected = expected.set_index("label") 34 | assert table.dataframe.equals(expected) 35 | -------------------------------------------------------------------------------- /tests/unit/tables/test_validators.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | import pytest 4 | from pandas import DataFrame 5 | 6 | from ngio.tables._validators import ( 7 | validate_columns, 8 | validate_table, 9 | validate_unique_index, 10 | ) 11 | from ngio.utils import NgioTableValidationError 12 | 13 | 14 | def test_validate_columns(): 15 | test_df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) 16 | required_columns = ["a", "b"] 17 | optional_columns = ["c"] 18 | result = validate_columns(test_df, required_columns, optional_columns) 19 | assert result.equals(test_df) 20 | 21 | with pytest.raises(NgioTableValidationError): 22 | validate_columns(test_df, ["a", "b", "d"], optional_columns) 23 | 24 | with pytest.raises(NgioTableValidationError): 25 | validate_columns(test_df, ["a", "b"], optional_columns=["d"]) 26 | 27 | 28 | def test_validate_index(): 29 | test_df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}) 30 | result = validate_unique_index(test_df) 31 | assert result.equals(test_df) 32 | 33 | test_df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}, index=[1, 1, 3]) 34 | with pytest.raises(NgioTableValidationError): 35 | validate_unique_index(test_df) 36 | 37 | 38 | def test_validate_table(): 39 | test_df = DataFrame({"a": [1, 2, 3], "b": [4, 5, 6], "c": [7, 8, 9]}) 40 | required_columns = ["a", "b"] 41 | optional_columns = ["c"] 42 | 43 | validators = [ 44 | partial( 45 | validate_columns, 46 | required_columns=required_columns, 47 | optional_columns=optional_columns, 48 | ), 49 | validate_unique_index, 50 | ] 51 | 52 | out_df = validate_table(test_df, validators) 53 | assert out_df.equals(test_df) 54 | -------------------------------------------------------------------------------- /tests/unit/utils/test_download_datasets.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | from ngio.utils import download_ome_zarr_dataset, list_ome_zarr_datasets 6 | 7 | 8 | def test_list_datasets(): 9 | assert len(list_ome_zarr_datasets()) > 0 10 | 11 | 12 | def test_fail_download_ome_zarr_dataset(tmp_path: Path): 13 | tmp_path = Path(tmp_path) / "test_datasets_fail" 14 | with pytest.raises(ValueError): 15 | download_ome_zarr_dataset("unknown_dataset", download_dir=tmp_path) 16 | 17 | assert not tmp_path.exists() 18 | --------------------------------------------------------------------------------