├── .github └── workflows │ ├── ci.yml │ ├── quarto-render.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── _quarto.yml ├── documentation ├── examples │ ├── OMETiff-Converter-Demo.qmd │ └── OMEZarr-Converter-Demo.qmd └── index.qmd ├── examples ├── OMETiff-convertor-demo.ipynb └── OMEZarr-convertor-demo.ipynb ├── pyproject.toml ├── quarto-materials ├── Background-tdb-header.jpg ├── _variables.scss ├── doc.svg ├── favicon.png ├── getTheme.html ├── react.html ├── tiledb-logo.png ├── tiledb-logo.svg ├── tiledb.css └── tiledb.scss ├── setup.cfg ├── setup.py ├── spec └── specification.md ├── tests ├── __init__.py ├── data │ ├── CMU-1-Small-Region-rgb.ome.tiff │ ├── CMU-1-Small-Region.ome.tiff │ ├── CMU-1-Small-Region.ome.zarr │ │ ├── 0 │ │ │ ├── 0 │ │ │ │ ├── 0 │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ │ └── 2 │ │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ │ └── 2 │ │ │ │ │ │ │ └── 2 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ │ └── 2 │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ │ └── 2 │ │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ │ └── 2 │ │ │ │ │ │ │ └── 2 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ │ └── 2 │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 2 │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 2 │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 2 │ │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ │ ├── 0 │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ │ ├── 1 │ │ │ ├── 0 │ │ │ │ ├── 0 │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ │ ├── 0 │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ │ ├── 2 │ │ │ ├── 0 │ │ │ │ ├── 0 │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ └── 1 │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ │ └── 1 │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 1 │ │ │ │ └── .zarray │ │ │ ├── 1 │ │ │ │ ├── 0 │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ └── .zarray │ │ │ ├── 2 │ │ │ │ ├── 0 │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ └── .zarray │ │ │ ├── 3 │ │ │ │ ├── 0 │ │ │ │ │ ├── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ ├── 1 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ │ │ └── 0 │ │ │ │ │ └── 2 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ │ │ └── 0 │ │ │ │ └── .zarray │ │ │ ├── .zattrs │ │ │ └── .zgroup │ │ ├── .zattrs │ │ ├── .zgroup │ │ └── OME │ │ │ ├── .zattrs │ │ │ ├── .zgroup │ │ │ └── METADATA.ome.xml │ ├── CMU-1-Small-Region.svs │ ├── UTM2GTIF.tiff │ ├── artificial-ome-tiff │ │ ├── 4D-series.ome.tif │ │ ├── multi-channel-4D-series.ome.tif │ │ ├── multi-channel-time-series.ome.tif │ │ ├── multi-channel-z-series.ome.tif │ │ ├── multi-channel.ome.tif │ │ ├── single-channel.ome.tif │ │ ├── time-series.ome.tif │ │ └── z-series.ome.tif │ ├── pngs │ │ ├── PNG_1_L.png │ │ ├── PNG_2_RGB.png │ │ └── PNG_2_RGBA.png │ └── rand_uint16.ome.tiff ├── integration │ ├── __init__.py │ ├── converters │ │ ├── __init__.py │ │ ├── test_ome_tiff.py │ │ ├── test_ome_tiff_experimental.py │ │ ├── test_ome_zarr.py │ │ ├── test_openslide.py │ │ ├── test_png.py │ │ └── test_scaler.py │ └── test_wrappers.py └── unit │ ├── __init__.py │ ├── test_axes.py │ ├── test_helpers.py │ ├── test_openslide.py │ └── test_tiles.py └── tiledb └── bioimg ├── __init__.py ├── converters ├── __init__.py ├── axes.py ├── base.py ├── io.py ├── metadata.py ├── ome_tiff.py ├── ome_zarr.py ├── openslide.py ├── png.py ├── scale.py └── tiles.py ├── helpers.py ├── openslide.py ├── plugin_manager.py ├── types.py └── wrappers.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: TileDB-BioImaging CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | name: ${{ matrix.sys.os }} 8 | runs-on: ${{ matrix.sys.os }} 9 | timeout-minutes: 25 10 | strategy: 11 | matrix: 12 | sys: 13 | - { os: windows-latest, shell: 'cmd /C call {0}' } 14 | - { os: ubuntu-24.04, shell: "bash -l {0}" } 15 | python-version: [3.9, 3.12] 16 | fail-fast: false 17 | defaults: 18 | run: 19 | shell: ${{ matrix.sys.shell }} 20 | 21 | env: 22 | run_coverage: ${{ github.ref == 'refs/heads/main' }} 23 | 24 | outputs: 25 | coverage: ${{ steps.stats.outputs.coverage }} 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | 30 | - name: Set up Python ${{ matrix.python-version }} 31 | uses: actions/setup-python@v2 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | - name: Install Conda environment with Micromamba 36 | uses: mamba-org/setup-micromamba@v2.0.2 37 | with: 38 | micromamba-version: latest 39 | environment-name: test 40 | cache-downloads: true 41 | create-args: >- 42 | pre-commit 43 | pytest-cov 44 | pytest-mock 45 | 46 | - name: Run pre-commit hooks 47 | run: | 48 | micromamba run -n test pre-commit run -a 49 | 50 | - name: Install package 51 | shell: "bash -l {0}" 52 | run: | 53 | pip install --no-cache-dir --upgrade tifffile "imagecodecs>=2023.7.10" 54 | pip install -e .[full] 55 | 56 | - name: Run tests with coverage 57 | id: stats 58 | env: 59 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 60 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 61 | run: | 62 | : # This GITHUB_WORKSPACE is bydefault set to D: driver whereas pytest's tmp_dir 63 | : # default is C: ,thus we create a temp_test folder for pytest's tmp_dir to run on D: as well 64 | pytest -v --cov=tiledb --cov-report=term-missing --durations=0 tests/ > coverage.txt 65 | exit_code=$? 66 | TEST_COVERAGE="$(grep '^TOTAL' coverage.txt | awk -v N=4 '{print $N}')" 67 | echo "coverage=$TEST_COVERAGE" >> $GITHUB_OUTPUT 68 | exit $exit_code 69 | if: ${{ matrix.sys.os != 'windows-latest' }} 70 | 71 | - name: Run tests with coverage WINDOWS 72 | id: stats-win 73 | env: 74 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 75 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 76 | run: | 77 | mkdir test_temp 78 | micromamba run -n test pytest --basetemp=test_temp -v --cov=tiledb --cov-report=term-missing --durations=0 tests/ --ignore=tests/integration/converters/test_ome_tiff_experimental.py 79 | if: ${{ matrix.sys.os == 'windows-latest' }} 80 | 81 | - name: Run notebook examples 82 | run: | 83 | micromamba run -n test pip install opencv-python-headless matplotlib nbmake 84 | micromamba run -n test pytest --nbmake examples 85 | 86 | - name: Create Test Coverage Badge 87 | if: ${{ env.run_coverage == 'true' && matrix.sys.os == 'ubuntu-24.04' }} 88 | uses: schneegans/dynamic-badges-action@v1.7.0 89 | with: 90 | auth: ${{ secrets.COVERAGE_SECRET }} 91 | gistID: 32d48185733a4e7375e80e3e35fab452 92 | filename: gist_bioimg.json 93 | label: Test Coverage 94 | message: ${{ steps.stats.outputs.coverage }} 95 | color: green 96 | namedLogo: pytest -------------------------------------------------------------------------------- /.github/workflows/quarto-render.yml: -------------------------------------------------------------------------------- 1 | # Cloned from https://github.com/TileDB-Inc/tiledb-quarto-template 2 | 3 | name: Render and deploy Quarto files 4 | on: 5 | push: 6 | pull_request: 7 | 8 | jobs: 9 | quarto-render-and-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: "Install Quarto" 15 | uses: quarto-dev/quarto-actions/setup@v2 16 | 17 | - name: "Quarto render" 18 | shell: bash 19 | run: | 20 | pip install tiledb-bioimg[full] quartodoc "pydantic<2" griffe==0.32.3 21 | quartodoc build 22 | quarto render --fail-if-warnings 23 | # https://github.com/quarto-dev/quarto-cli/issues/493 24 | 25 | - name: "Deploy to gh-pages" 26 | uses: peaceiris/actions-gh-pages@v3 27 | # Change to the name of your repo's primary branch name: 28 | if: github.ref == 'refs/heads/main' && github.repository_owner == 'TileDB-Inc' 29 | with: 30 | # This is GitHub Actions magic; no secrets for us to manage; and this works first-time 31 | # without any extra configs other than visiting Settings -> Pages in your GitHub repo. 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: docs 34 | destination_dir: docs -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: [ 'published' ] 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Setup Python 11 | uses: actions/setup-python@v2 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Build package 15 | run: pip install wheel setuptools && python setup.py sdist bdist_wheel && ls -l dist 16 | - name: Publish package to TestPyPI 17 | uses: pypa/gh-action-pypi-publish@release/v1 18 | continue-on-error: true 19 | with: 20 | user: __token__ 21 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 22 | repository_url: https://test.pypi.org/legacy/ 23 | - name: Publish package to PyPI 24 | if: "!github.event.release.prerelease" 25 | uses: pypa/gh-action-pypi-publish@release/v1 26 | with: 27 | user: __token__ 28 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | .eggs/ 4 | *.egg-info/ 5 | build/ 6 | dist/ 7 | *.DS_Store 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | 13 | # setuptools-scm 14 | tiledb/bioimg/version.py 15 | 16 | # Unit test / coverage reports 17 | .coverage 18 | 19 | # Docs generated files 20 | /.quarto/ 21 | docs/ 22 | documentation/api 23 | documentation/examples/*Demo_files 24 | objects.json 25 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/ambv/black 3 | rev: 24.4.2 4 | hooks: 5 | - id: black 6 | - repo: https://github.com/charliermarsh/ruff-pre-commit 7 | rev: v0.4.2 8 | hooks: 9 | - id: ruff 10 | - repo: https://github.com/pre-commit/mirrors-mypy 11 | rev: v1.10.0 12 | hooks: 13 | - id: mypy 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 TileDB, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TileDB logo 2 | 3 | [![TileDB-BioImaging CI](https://github.com/TileDB-Inc/TileDB-BioImaging/actions/workflows/ci.yml/badge.svg)](https://github.com/TileDB-Inc/TileDB-BioImaging/actions/workflows/ci.yml) 4 | ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/ktsitsi/32d48185733a4e7375e80e3e35fab452/raw/gist_bioimg.json) 5 | 6 | # TileDB-BioImaging 7 | 8 | Python package for: 9 | - converting images stored in popular Biomedical Imaging formats to groups of TileDB arrays (& vice versa) 10 | - exposing an expressive and efficient API (powered by TileDB) for querying such data. 11 | 12 | ## Features 13 | 14 | ### Ingestion to TileDB Groups of Arrays 15 | - OME-Zarr 16 | - OME-Tiff 17 | - Open-Slide 18 | 19 | ### Export from TileDB-Bioimaging Arrays to: 20 | - OME-Zarr 21 | - OME-Tiff 22 | 23 | ### Visualization Options 24 | 25 | - [TileDB Cloud](https://cloud.tiledb.com) includes a built-in, pyramidal multi-resolution viewer: log in to TileDB Cloud to see an example image preview [here](https://cloud.tiledb.com/biomedical-imaging/TileDB-Inc/dbb7dfcc-28b3-40e5-916f-6509a666d950/preview) 26 | - Napari: https://github.com/TileDB-Inc/napari-tiledb-bioimg 27 | 28 | ## Quick Installation 29 | 30 | - From PyPI: 31 | 32 | pip install 'tiledb-bioimg[full]' 33 | 34 | - From source: 35 | 36 | git clone https://github.com/TileDB-Inc/TileDB-BioImaging.git 37 | cd TileDB-BioImaging 38 | 39 | pip install -e '.[full]' 40 | 41 | 42 | ## Examples 43 | How to convert imaging data from standard biomedical formats to group of TileDB arrays. 44 | 45 | ### OME-Zarr to TileDB Group of Arrays 46 | ```python 47 | from tiledb.bioimg.converters.ome_zarr import OMEZarrConverter 48 | OMEZarrConverter.to_tiledb("path_to_ome_zarr_image", "tiledb_array_group_path") 49 | ``` 50 | 51 | ### OME-Tiff to TileDB Group of Arrays 52 | ```python 53 | from tiledb.bioimg.converters.ome_tiff import OMETiffConverter 54 | OMETiffConverter.to_tiledb("path_to_ome_tiff_image", "tiledb_array_group_path") 55 | ``` 56 | 57 | ### Open Slide to TileDB Group of Arrays 58 | ```python 59 | from tiledb.bioimg.converters.openslide import OpenSlideConverter 60 | OpenSlideConverter.to_tiledb("path_to_open_slide_image", "tiledb_array_group_path") 61 | ``` 62 | 63 | ## Documentation 64 | `API Documentation` is auto-generated. Following the instructions below: 65 | 66 | ```shell 67 | quartodoc build && quarto preview 68 | ``` 69 | -------------------------------------------------------------------------------- /_quarto.yml: -------------------------------------------------------------------------------- 1 | project: 2 | type: website 3 | output-dir: docs 4 | render: 5 | - "documentation/index.qmd" 6 | - "documentation/" 7 | 8 | format: 9 | html: 10 | include-in-header: 11 | - file: quarto-materials/getTheme.html 12 | theme: 13 | light: 14 | - quarto-materials/_variables.scss 15 | mainfont: Inter var, sans-serif 16 | fontsize: 1rem 17 | linkcolor: '#4d9fff' 18 | strip-comments: true 19 | toc: true 20 | toc-expand: true 21 | notebook-links: false 22 | code-copy: true 23 | code-overflow: wrap 24 | css: quarto-materials/tiledb.scss 25 | include-after-body: quarto-materials/react.html 26 | page-layout: full 27 | grid: 28 | sidebar-width: 280px 29 | margin-width: 0px 30 | body-width: 10000px 31 | 32 | quartodoc: 33 | style: pkgdown 34 | parser: "sphinx" 35 | package: tiledb 36 | dir: "documentation/api" 37 | renderer: 38 | style: markdown 39 | display_name: relative 40 | sections: 41 | - title: "Converters" 42 | desc: "" 43 | package: tiledb.bioimg.converters 44 | options: 45 | include_inherited: true 46 | contents: 47 | - name: ome_tiff.OMETiffConverter 48 | - name: ome_zarr.OMEZarrConverter 49 | - name: openslide.OpenSlideConverter 50 | 51 | - title: "TileDBOpenslide" 52 | desc: "" 53 | package: tiledb.bioimg.openslide 54 | contents: 55 | - name: TileDBOpenSlide 56 | 57 | - title: "Ingestion" 58 | desc: "" 59 | package: tiledb.bioimg.wrappers 60 | contents: 61 | - name: from_bioimg 62 | 63 | - title: "Exporation" 64 | desc: "" 65 | package: tiledb.bioimg.wrappers 66 | contents: 67 | - name: to_bioimg 68 | 69 | website: 70 | favicon: "images/favicon.ico" 71 | site-url: https://tiledb-inc.github.io/tiledb-quarto-template/ 72 | repo-url: https://github.com/TileDB-Inc/tiledb-quarto-template 73 | 74 | repo-actions: [issue] 75 | page-navigation: true 76 | navbar: 77 | background: light 78 | logo: "quarto-materials/tiledb-logo.png" 79 | collapse-below: lg 80 | left: 81 | - text: "Home page" 82 | href: "https://tiledb.com" 83 | - text: "Login" 84 | href: "https://cloud.tiledb.com/auth/login" 85 | - text: "Contact us" 86 | href: "https://tiledb.com/contact" 87 | - text: "Repo" 88 | href: "https://github.com/TileDB-Inc/TileDB-BioImaging" 89 | 90 | sidebar: 91 | - style: "floating" 92 | collapse-level: 2 93 | align: left 94 | contents: 95 | 96 | - section: "Overview" 97 | contents: 98 | - href: "documentation/index.qmd" 99 | # 100 | - section: "Examples" 101 | contents: 102 | - href: "documentation/examples/OMETiff-Converter-Demo.qmd" 103 | - href: "documentation/examples/OMEZarr-Converter-Demo.qmd" 104 | # 105 | - section: "API Reference" 106 | contents: 107 | - href: "documentation/api/index.qmd" 108 | -------------------------------------------------------------------------------- /documentation/examples/OMETiff-Converter-Demo.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OME Tiff" 3 | description: "Learn how to ingest and perform basic ML operations on the MNIST dense dataset." 4 | --- 5 | 6 | 7 | {{< embed ../../examples/OMETiff-convertor-demo.ipynb echo=true >}} 8 | -------------------------------------------------------------------------------- /documentation/examples/OMEZarr-Converter-Demo.qmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "OME Zarr" 3 | description: "Learn how to ingest and perform basic ML operations on the MNIST dense dataset." 4 | --- 5 | 6 | 7 | {{< embed ../../examples/OMEZarr-convertor-demo.ipynb echo=true >}} 8 | -------------------------------------------------------------------------------- /documentation/index.qmd: -------------------------------------------------------------------------------- 1 | TileDB logo 2 | 3 | [![TileDB-BioImaging CI](https://github.com/TileDB-Inc/TileDB-BioImaging/actions/workflows/ci.yml/badge.svg)](https://github.com/TileDB-Inc/TileDB-BioImaging/actions/workflows/ci.yml) 4 | ![Coverage Badge](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/ktsitsi/32d48185733a4e7375e80e3e35fab452/raw/gist_bioimg.json) 5 | 6 | # TileDB-BioImaging 7 | 8 | Python package for: 9 | - converting images stored in popular Biomedical Imaging formats to groups of TileDB arrays (& vice versa) 10 | - exposing an expressive and efficient API (powered by TileDB) for querying such data. 11 | 12 | **Note**: this project is in an early stage and under heavy development. 13 | Breaking changes to the underlying data format and the API are to be expected. 14 | 15 | ## Features 16 | 17 | ### Ingestion to TileDB Groups of Arrays 18 | - OME-Zarr 19 | - OME-Tiff 20 | - Open-Slide 21 | 22 | ### Export from TileDB-Bioimaging Arrays to: 23 | - OME-Zarr 24 | - OME-Tiff 25 | 26 | ### Visualization Options 27 | 28 | - [TileDB Cloud](https://cloud.tiledb.com) includes a built-in, pyramidal multi-resolution viewer: log in to TileDB Cloud to see an example image preview [here](https://cloud.tiledb.com/biomedical-imaging/TileDB-Inc/dbb7dfcc-28b3-40e5-916f-6509a666d950/preview) 29 | - Napari: https://github.com/TileDB-Inc/napari-tiledb-bioimg 30 | 31 | ## Quick Installation 32 | 33 | - From PyPI: 34 | 35 | pip install 'tiledb-bioimg[full]' 36 | 37 | - From source: 38 | 39 | git clone https://github.com/TileDB-Inc/TileDB-BioImaging.git 40 | cd TileDB-BioImaging 41 | 42 | pip install -e '.[full]' 43 | 44 | 45 | ## Examples 46 | How to convert imaging data from standard biomedical formats to group of TileDB arrays. 47 | 48 | ### OME-Zarr to TileDB Group of Arrays 49 | ```python 50 | from tiledb.bioimg.converters.ome_zarr import OMEZarrConverter 51 | OMEZarrConverter.to_tiledb("path_to_ome_zarr_image", "tiledb_array_group_path") 52 | ``` 53 | 54 | ### OME-Tiff to TileDB Group of Arrays 55 | ```python 56 | from tiledb.bioimg.converters.ome_tiff import OMETiffConverter 57 | OMETiffConverter.to_tiledb("path_to_ome_tiff_image", "tiledb_array_group_path") 58 | ``` 59 | 60 | ### Open Slide to TileDB Group of Arrays 61 | ```python 62 | from tiledb.bioimg.converters.openslide import OpenSlideConverter 63 | OpenSlideConverter.to_tiledb("path_to_open_slide_image", "tiledb_array_group_path") 64 | ``` 65 | 66 | ## Documentation 67 | `API Documentation` is auto-generated. Following the instructions below: 68 | 69 | ```shell 70 | quartodoc build && quarto preview 71 | ``` 72 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.mypy] 2 | show_error_codes = true 3 | warn_unreachable = true 4 | namespace_packages = false 5 | strict = true 6 | 7 | [[tool.mypy.overrides]] 8 | module = "tests.*" 9 | ignore_errors = true 10 | 11 | [tool.ruff] 12 | lint.ignore = ["E501"] 13 | lint.extend-select = ["I001"] 14 | exclude = ["__init__.py"] 15 | fix = true 16 | -------------------------------------------------------------------------------- /quarto-materials/Background-tdb-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/quarto-materials/Background-tdb-header.jpg -------------------------------------------------------------------------------- /quarto-materials/doc.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /quarto-materials/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/quarto-materials/favicon.png -------------------------------------------------------------------------------- /quarto-materials/getTheme.html: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /quarto-materials/react.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | -------------------------------------------------------------------------------- /quarto-materials/tiledb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/quarto-materials/tiledb-logo.png -------------------------------------------------------------------------------- /quarto-materials/tiledb-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /quarto-materials/tiledb.css: -------------------------------------------------------------------------------- 1 | /* 2 | Cloned from https://github.com/TileDB-Inc/tiledb-quarto-template 3 | 4 | tiledb light blue #4d9fff 5 | tiledb dark blue #0a2580 6 | */ 7 | 8 | .navbar-nav:hover .nav-link:hover { 9 | color: #4d9fff; 10 | } 11 | 12 | .nav-page:hover .nav-page-previous:hover { 13 | color: #4d9fff; 14 | } 15 | 16 | .nav-page:hover .nav-page-next:hover { 17 | color: #4d9fff; 18 | } 19 | 20 | .nav-page:hover .nav-page-text:hover { 21 | color: #4d9fff; 22 | } 23 | 24 | .toc-actions a:hover { 25 | color: #4d9fff; 26 | } 27 | 28 | .page-navigation:hover { 29 | color: #4d9fff; 30 | } 31 | 32 | a.pagination-link:hover { 33 | color: #4d9fff; 34 | } 35 | 36 | .sidebar-navigation .text-start { 37 | font-weight: bold; 38 | } 39 | 40 | .sidebar.sidebar-navigation .active { 41 | /* 42 | color: #800000; 43 | background-color: #e0e0e0; 44 | */ 45 | } 46 | 47 | .sidebar.sidebar-navigation .active, 48 | .sidebar.sidebar-navigation .show > .nav-link { 49 | /*color: #0a2580;*/ 50 | color: #2c4396; 51 | background-color: #e0e0e0; 52 | padding-left: 4px; 53 | padding-right: 4px; 54 | } 55 | 56 | a { 57 | color: #2c4396; 58 | } 59 | a:before, 60 | a:focus, 61 | a:hover, 62 | a:link, 63 | a:visited { 64 | color: #4629c9; 65 | } 66 | 67 | code, 68 | p code:not(.sourceCode), 69 | li code:not(.sourceCode), 70 | kbd, 71 | pre { 72 | color: #000000; 73 | background-color: #f0f0f0; 74 | font-size: 12px; 75 | direction: ltr; 76 | border-radius: 3px; 77 | } 78 | 79 | pre { 80 | font-size: 12px; 81 | padding: 10px; 82 | text-decoration: none; 83 | 84 | white-space: pre-wrap; /* css-3 */ 85 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 86 | white-space: -pre-wrap; /* Opera 4-6 */ 87 | white-space: -o-pre-wrap; /* Opera 7 */ 88 | } 89 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = tiledb-bioimg 3 | description = Package supports all bio-imaging functionality provided by TileDB 4 | author = TileDB, Inc. 5 | author_email = help@tiledb.io 6 | long_description = file: README.md 7 | long_description_content_type = text/markdown 8 | license = MIT 9 | license_file = LICENSE 10 | keywords = tiledb, bioimaging 11 | url = https://github.com/TileDB-Inc/TileDB-Bioimaging 12 | platform = any 13 | project_urls = 14 | Bug Tracker = https://github.com/TileDB-Inc/TileDB-Bioimaging/issues 15 | Documentation = https://docs.tiledb.com 16 | classifiers = 17 | Development Status :: 2 - Pre-Alpha 18 | Intended Audience :: Developers 19 | Intended Audience :: Information Technology 20 | Intended Audience :: Science/Research 21 | Programming Language :: Python 22 | Topic :: Software Development :: Libraries :: Python Modules 23 | Topic :: Scientific/Engineering :: Bio-Informatics 24 | Topic :: Scientific/Engineering :: Image Processing 25 | Operating System :: Unix 26 | Operating System :: POSIX :: Linux 27 | Operating System :: MacOS :: MacOS X 28 | Programming Language :: Python :: 3 29 | Programming Language :: Python :: 3.7 30 | Programming Language :: Python :: 3.8 31 | License :: OSI Approved :: MIT License 32 | 33 | [options] 34 | zip_safe = False 35 | packages = find_namespace: 36 | python_requires = >=3.8 37 | 38 | 39 | [options.packages.find] 40 | exclude = 41 | tests* 42 | docs* 43 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | zarr = ["ome-zarr>=0.9.0"] 4 | openslide = ["openslide-python"] 5 | tiff = ["tifffile", "imagecodecs"] 6 | cloud = ["tiledb-cloud"] 7 | 8 | full = sorted({*zarr, *openslide, *tiff}) 9 | setuptools.setup( 10 | setup_requires=["setuptools_scm"], 11 | use_scm_version={ 12 | "version_scheme": "guess-next-dev", 13 | "local_scheme": "dirty-tag", 14 | "write_to": "tiledb/bioimg/version.py", 15 | }, 16 | install_requires=[ 17 | "openslide-bin", 18 | "pyeditdistance", 19 | "tiledb>=0.19", 20 | "tqdm", 21 | "scikit-image", 22 | "jsonpickle", 23 | "requires", 24 | ], 25 | extras_require={ 26 | "zarr": zarr, 27 | "openslide": openslide, 28 | "tiff": tiff, 29 | "cloud": cloud, 30 | "full": full, 31 | }, 32 | entry_points={ 33 | "bioimg.readers": [ 34 | "tiff_reader = tiledb.bioimg.converters.ome_tiff:OMETiffReader", 35 | "zarr_reader = tiledb.bioimg.converters.ome_zarr:OMEZarrReader", 36 | "osd_reader = tiledb.bioimg.converters.openslide:OpenSlideReader", 37 | "png_reader = tiledb.bioimg.converters.png.PNGReader", 38 | ], 39 | "bioimg.writers": [ 40 | "tiff_writer = tiledb.bioimg.converters.ome_tiff:OMETiffWriter", 41 | "zarr_writer = tiledb.bioimg.converters.ome_tiff:OMEZarrWriter", 42 | "png_writer = tiledb.bioimg.converters.png.PNGWriter", 43 | ], 44 | "bioimg.converters": [ 45 | "tiff_converter = tiledb.bioimg.converters.ome_tiff:OMETiffConverter", 46 | "zarr_converter = tiledb.bioimg.converters.ome_zarr:OMEZarrConverter", 47 | "osd_converter = tiledb.bioimg.converters.openslide:OpenSlideConverter", 48 | "png_converter = tiledb.bioimg.converters.png:PNGConverter", 49 | ], 50 | }, 51 | ) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import random 4 | import numpy as np 5 | from skimage.metrics import structural_similarity 6 | 7 | import tiledb 8 | from tiledb.bioimg import ATTR_NAME 9 | from tiledb.filter import WebpFilter 10 | from tiledb.bioimg.helpers import merge_ned_ranges 11 | import xml.etree.ElementTree as ET 12 | 13 | DATA_DIR = Path(__file__).parent / "data" 14 | 15 | 16 | def get_schema(x_size, y_size, c_size=3, compressor=tiledb.ZstdFilter(level=0)): 17 | dims = [] 18 | x_tile = min(x_size, 1024) 19 | y_tile = min(y_size, 1024) 20 | # WEBP Compressor does not accept specific dtypes so for dimensions we use the default 21 | dim_compressor = tiledb.ZstdFilter(level=0) 22 | if not isinstance(compressor, tiledb.WebpFilter): 23 | dim_compressor = compressor 24 | if isinstance(compressor, tiledb.WebpFilter): 25 | x_size *= c_size 26 | x_tile *= c_size 27 | if compressor.input_format == WebpFilter.WebpInputFormat.WEBP_NONE: 28 | if c_size == 3: 29 | input_format = WebpFilter.WebpInputFormat.WEBP_RGB 30 | elif c_size == 4: 31 | input_format = WebpFilter.WebpInputFormat.WEBP_RGBA 32 | else: 33 | assert False, f"No WebpInputFormat with pixel_depth={c_size}" 34 | compressor = tiledb.WebpFilter( 35 | input_format=input_format, 36 | quality=compressor.quality, 37 | lossless=compressor.lossless, 38 | ) 39 | else: 40 | if c_size > 1: 41 | dims.append( 42 | tiledb.Dim( 43 | "C", 44 | (0, c_size - 1), 45 | tile=c_size, 46 | dtype=np.uint32, 47 | filters=tiledb.FilterList([compressor]), 48 | ) 49 | ) 50 | 51 | dims.append( 52 | tiledb.Dim( 53 | "Y", 54 | (0, y_size - 1), 55 | tile=y_tile, 56 | dtype=np.uint32, 57 | filters=tiledb.FilterList([dim_compressor]), 58 | ) 59 | ) 60 | dims.append( 61 | tiledb.Dim( 62 | "X", 63 | (0, x_size - 1), 64 | tile=x_tile, 65 | dtype=np.uint32, 66 | filters=tiledb.FilterList([dim_compressor]), 67 | ) 68 | ) 69 | 70 | return tiledb.ArraySchema( 71 | domain=tiledb.Domain(*dims), 72 | attrs=[ 73 | tiledb.Attr( 74 | name=ATTR_NAME, dtype=np.uint8, filters=tiledb.FilterList([compressor]) 75 | ) 76 | ], 77 | ) 78 | 79 | 80 | def get_path(uri): 81 | return DATA_DIR / uri 82 | 83 | 84 | def assert_image_similarity(im1, im2, min_threshold=0.95, channel_axis=-1, win_size=11): 85 | s = structural_similarity(im1, im2, channel_axis=channel_axis, win_size=win_size) 86 | assert s >= min_threshold, (s, min_threshold, im1.shape) 87 | 88 | 89 | def generate_test_case(num_axes, num_ranges, max_value): 90 | """ 91 | Generate a test case with a given number of axes and ranges. 92 | 93 | Parameters: 94 | num_axes (int): Number of axes. 95 | num_ranges (int): Number of ranges. 96 | max_value (int): Maximum value for range endpoints. 97 | 98 | Returns: 99 | tuple: A tuple containing the generated input and the expected output. 100 | """ 101 | input_ranges = [] 102 | 103 | for _ in range(num_ranges): 104 | ranges = [] 105 | for _ in range(num_axes): 106 | start = random.randint(0, max_value - 1) 107 | end = random.randint(start, max_value) 108 | ranges.append((start, end)) 109 | input_ranges.append(tuple(ranges)) 110 | 111 | input_ranges = tuple(input_ranges) 112 | 113 | expected_output = merge_ned_ranges(input_ranges) 114 | 115 | return input_ranges, expected_output 116 | 117 | 118 | def generate_xml( 119 | has_macro=True, has_label=True, has_annotations=True, root_tag="OME", num_images=1 120 | ): 121 | """Generate synthetic XML strings with options to include 'macro' and 'label' images.""" 122 | 123 | # Create the root element 124 | ome = ET.Element( 125 | root_tag, 126 | { 127 | "xmlns": "http://www.openmicroscopy.org/Schemas/OME/2016-06", 128 | "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", 129 | "Creator": "tifffile.py 2023.7.4", 130 | "UUID": "urn:uuid:40348664-c1f8-11ee-a19b-58112295faaf", 131 | "xsi:schemaLocation": "http://www.openmicroscopy.org/Schemas/OME/2016-06 http://www.openmicroscopy.org/Schemas/OME/2016-06/ome.xsd", 132 | }, 133 | ) 134 | 135 | # Create an instrument element 136 | instrument = ET.SubElement(ome, "Instrument", ID="Instrument:95") 137 | objective = ET.SubElement( 138 | instrument, "Objective", ID="Objective:95", NominalMagnification="40.0" 139 | ) 140 | 141 | # Create standard image elements 142 | for i in range(num_images): 143 | image = ET.SubElement(ome, "Image", ID=f"Image:{i}", Name=f"Image{i}") 144 | pixels = ET.SubElement( 145 | image, 146 | "Pixels", 147 | DimensionOrder="XYCZT", 148 | ID=f"Pixels:{i}", 149 | SizeC="3", 150 | SizeT="1", 151 | SizeX="86272", 152 | SizeY="159488", 153 | SizeZ="1", 154 | Type="uint8", 155 | Interleaved="true", 156 | PhysicalSizeX="0.2827", 157 | PhysicalSizeY="0.2827", 158 | ) 159 | channel = ET.SubElement( 160 | pixels, "Channel", ID=f"Channel:{i}:0", SamplesPerPixel="3" 161 | ) 162 | tiffdata = ET.SubElement(pixels, "TiffData", PlaneCount="1") 163 | 164 | # Conditionally add 'macro' and 'label' images 165 | if has_label: 166 | label_image = ET.SubElement(ome, "Image", ID="Image:label", Name="label") 167 | pixels = ET.SubElement( 168 | label_image, 169 | "Pixels", 170 | DimensionOrder="XYCZT", 171 | ID="Pixels:label", 172 | SizeC="3", 173 | SizeT="1", 174 | SizeX="604", 175 | SizeY="594", 176 | SizeZ="1", 177 | Type="uint8", 178 | Interleaved="true", 179 | PhysicalSizeX="43.0", 180 | PhysicalSizeY="43.0", 181 | ) 182 | channel = ET.SubElement( 183 | pixels, "Channel", ID="Channel:label:0", SamplesPerPixel="3" 184 | ) 185 | tiffdata = ET.SubElement(pixels, "TiffData", IFD="1", PlaneCount="1") 186 | 187 | if has_macro: 188 | macro_image = ET.SubElement(ome, "Image", ID="Image:macro", Name="macro") 189 | pixels = ET.SubElement( 190 | macro_image, 191 | "Pixels", 192 | DimensionOrder="XYCZT", 193 | ID="Pixels:macro", 194 | SizeC="3", 195 | SizeT="1", 196 | SizeX="604", 197 | SizeY="1248", 198 | SizeZ="1", 199 | Type="uint8", 200 | Interleaved="true", 201 | PhysicalSizeX="43.0", 202 | PhysicalSizeY="43.0", 203 | ) 204 | channel = ET.SubElement( 205 | pixels, "Channel", ID="Channel:macro:0", SamplesPerPixel="3" 206 | ) 207 | tiffdata = ET.SubElement(pixels, "TiffData", IFD="2", PlaneCount="1") 208 | 209 | if has_annotations: 210 | annotations = ET.SubElement(ome, "StructuredAnnotations") 211 | if has_macro: 212 | macro_annot = ET.SubElement( 213 | annotations, 214 | "CommentAnnotation", 215 | ID=f"Annotation:{random.randint}", 216 | Namespace="", 217 | ) 218 | description = ET.SubElement(macro_annot, "Description") 219 | 220 | # Based on standard 221 | description.text = "barcode_value" 222 | value = ET.SubElement(macro_annot, "Value") 223 | value.text = "random_text" 224 | if has_label: 225 | label_annot = ET.SubElement( 226 | annotations, 227 | "CommentAnnotation", 228 | ID=f"Annotation:{random.randint}", 229 | Namespace="", 230 | ) 231 | description = ET.SubElement( 232 | label_annot, 233 | "Description", 234 | ) 235 | value = ET.SubElement(label_annot, "Value") 236 | 237 | # Based on standard 238 | description.text = "label_text" 239 | value.text = "random_text" 240 | 241 | # Convert the ElementTree to a string 242 | return ET.tostring(ome, encoding="unicode") 243 | -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region-rgb.ome.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region-rgb.ome.tiff -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.tiff -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "bioformats2raw.layout" : 3 3 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format" : 2 3 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales" : [ { 3 | "metadata" : { 4 | "method" : "loci.common.image.SimpleImageScaler", 5 | "version" : "Bio-Formats 6.10.1" 6 | }, 7 | "axes" : [ { 8 | "name" : "t", 9 | "type" : "time" 10 | }, { 11 | "name" : "c", 12 | "type" : "channel" 13 | }, { 14 | "name" : "z", 15 | "type" : "space" 16 | }, { 17 | "unit" : "micrometer", 18 | "name" : "y", 19 | "type" : "space" 20 | }, { 21 | "unit" : "micrometer", 22 | "name" : "x", 23 | "type" : "space" 24 | } ], 25 | "name" : "", 26 | "datasets" : [ { 27 | "path" : "0", 28 | "coordinateTransformations" : [ { 29 | "scale" : [ 1.0, 1.0, 1.0, 0.499, 0.499 ], 30 | "type" : "scale" 31 | } ] 32 | }, { 33 | "path" : "1", 34 | "coordinateTransformations" : [ { 35 | "scale" : [ 1.0, 1.0, 1.0, 0.998, 0.998 ], 36 | "type" : "scale" 37 | } ] 38 | } ], 39 | "version" : "0.4" 40 | } ], 41 | "omero" : { 42 | "channels" : [ { 43 | "color" : "FF0000", 44 | "coefficient" : 1, 45 | "active" : true, 46 | "label" : "Channel 0", 47 | "window" : { 48 | "min" : 0.0, 49 | "max" : 255.0, 50 | "start" : 0.0, 51 | "end" : 255.0 52 | }, 53 | "family" : "linear", 54 | "inverted" : false 55 | }, { 56 | "color" : "00FF00", 57 | "coefficient" : 1, 58 | "active" : true, 59 | "label" : "Channel 1", 60 | "window" : { 61 | "min" : 0.0, 62 | "max" : 255.0, 63 | "start" : 0.0, 64 | "end" : 255.0 65 | }, 66 | "family" : "linear", 67 | "inverted" : false 68 | }, { 69 | "color" : "0000FF", 70 | "coefficient" : 1, 71 | "active" : true, 72 | "label" : "Channel 2", 73 | "window" : { 74 | "min" : 0.0, 75 | "max" : 255.0, 76 | "start" : 0.0, 77 | "end" : 255.0 78 | }, 79 | "family" : "linear", 80 | "inverted" : false 81 | } ], 82 | "rdefs" : { 83 | "defaultT" : 0, 84 | "model" : "color", 85 | "defaultZ" : 0 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format" : 2 3 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks" : [ 1, 1, 1, 1024, 1024 ], 3 | "compressor" : { 4 | "clevel" : 5, 5 | "blocksize" : 0, 6 | "shuffle" : 1, 7 | "cname" : "lz4", 8 | "id" : "blosc" 9 | }, 10 | "dtype" : "|u1", 11 | "fill_value" : 0, 12 | "filters" : null, 13 | "order" : "C", 14 | "shape" : [ 1, 3, 1, 2967, 2220 ], 15 | "dimension_separator" : "/", 16 | "zarr_format" : 2 17 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/0/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/0/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/0/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/0/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/1/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/1/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/1/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/1/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/1/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/1/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/2/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/2/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/2/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/2/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/2/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/0/0/2/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/0/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/0/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/0/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/0/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/1/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/1/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/1/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/1/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/1/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/1/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/2/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/2/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/2/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/2/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/2/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/1/0/2/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/0/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/0/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/0/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/0/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/1/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/1/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/1/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/1/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/1/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/1/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/2/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/2/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/2/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/2/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/2/2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/0/0/2/0/2/2 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks" : [ 1, 1, 1, 768, 574 ], 3 | "compressor" : { 4 | "clevel" : 5, 5 | "blocksize" : 0, 6 | "shuffle" : 1, 7 | "cname" : "lz4", 8 | "id" : "blosc" 9 | }, 10 | "dtype" : "|u1", 11 | "fill_value" : 0, 12 | "filters" : null, 13 | "order" : "C", 14 | "shape" : [ 1, 3, 1, 768, 574 ], 15 | "dimension_separator" : "/", 16 | "zarr_format" : 2 17 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/1/0/0/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/1/0/0/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/1/0/1/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/1/0/1/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/0/1/0/2/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/0/1/0/2/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales" : [ { 3 | "metadata" : { 4 | "method" : "loci.common.image.SimpleImageScaler", 5 | "version" : "Bio-Formats 6.10.1" 6 | }, 7 | "axes" : [ { 8 | "name" : "t", 9 | "type" : "time" 10 | }, { 11 | "name" : "c", 12 | "type" : "channel" 13 | }, { 14 | "name" : "z", 15 | "type" : "space" 16 | }, { 17 | "name" : "y", 18 | "type" : "space" 19 | }, { 20 | "name" : "x", 21 | "type" : "space" 22 | } ], 23 | "name" : "macro image", 24 | "datasets" : [ { 25 | "path" : "0", 26 | "coordinateTransformations" : [ { 27 | "scale" : [ 1.0, 1.0, 1.0, 1.0, 1.0 ], 28 | "type" : "scale" 29 | } ] 30 | }, { 31 | "path" : "1", 32 | "coordinateTransformations" : [ { 33 | "scale" : [ 1.0, 1.0, 1.0, 2.0, 2.0 ], 34 | "type" : "scale" 35 | } ] 36 | } ], 37 | "version" : "0.4" 38 | } ], 39 | "omero" : { 40 | "channels" : [ { 41 | "color" : "FF0000", 42 | "coefficient" : 1, 43 | "active" : true, 44 | "label" : "Channel 0", 45 | "window" : { 46 | "min" : 0.0, 47 | "max" : 255.0, 48 | "start" : 0.0, 49 | "end" : 255.0 50 | }, 51 | "family" : "linear", 52 | "inverted" : false 53 | }, { 54 | "color" : "00FF00", 55 | "coefficient" : 1, 56 | "active" : true, 57 | "label" : "Channel 1", 58 | "window" : { 59 | "min" : 0.0, 60 | "max" : 255.0, 61 | "start" : 0.0, 62 | "end" : 255.0 63 | }, 64 | "family" : "linear", 65 | "inverted" : false 66 | }, { 67 | "color" : "0000FF", 68 | "coefficient" : 1, 69 | "active" : true, 70 | "label" : "Channel 2", 71 | "window" : { 72 | "min" : 0.0, 73 | "max" : 255.0, 74 | "start" : 0.0, 75 | "end" : 255.0 76 | }, 77 | "family" : "linear", 78 | "inverted" : false 79 | } ], 80 | "rdefs" : { 81 | "defaultT" : 0, 82 | "model" : "color", 83 | "defaultZ" : 0 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format" : 2 3 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks" : [ 1, 1, 1, 463, 387 ], 3 | "compressor" : { 4 | "clevel" : 5, 5 | "blocksize" : 0, 6 | "shuffle" : 1, 7 | "cname" : "lz4", 8 | "id" : "blosc" 9 | }, 10 | "dtype" : "|u1", 11 | "fill_value" : 0, 12 | "filters" : null, 13 | "order" : "C", 14 | "shape" : [ 1, 3, 1, 463, 387 ], 15 | "dimension_separator" : "/", 16 | "zarr_format" : 2 17 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/0/0/0/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/1/0/0/0/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/0/0/1/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/1/0/0/1/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/0/0/2/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/1/0/0/2/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks" : [ 1, 1, 1, 231, 193 ], 3 | "compressor" : { 4 | "clevel" : 5, 5 | "blocksize" : 0, 6 | "shuffle" : 1, 7 | "cname" : "lz4", 8 | "id" : "blosc" 9 | }, 10 | "dtype" : "|u1", 11 | "fill_value" : 0, 12 | "filters" : null, 13 | "order" : "C", 14 | "shape" : [ 1, 3, 1, 231, 193 ], 15 | "dimension_separator" : "/", 16 | "zarr_format" : 2 17 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/1/0/0/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/1/1/0/0/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/1/0/1/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/1/1/0/1/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/1/1/0/2/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/1/1/0/2/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "multiscales" : [ { 3 | "metadata" : { 4 | "method" : "loci.common.image.SimpleImageScaler", 5 | "version" : "Bio-Formats 6.10.1" 6 | }, 7 | "axes" : [ { 8 | "name" : "t", 9 | "type" : "time" 10 | }, { 11 | "name" : "c", 12 | "type" : "channel" 13 | }, { 14 | "name" : "z", 15 | "type" : "space" 16 | }, { 17 | "name" : "y", 18 | "type" : "space" 19 | }, { 20 | "name" : "x", 21 | "type" : "space" 22 | } ], 23 | "name" : "label image", 24 | "datasets" : [ { 25 | "path" : "0", 26 | "coordinateTransformations" : [ { 27 | "scale" : [ 1.0, 1.0, 1.0, 1.0, 1.0 ], 28 | "type" : "scale" 29 | } ] 30 | }, { 31 | "path" : "1", 32 | "coordinateTransformations" : [ { 33 | "scale" : [ 1.0, 1.0, 1.0, 2.0, 2.0 ], 34 | "type" : "scale" 35 | } ] 36 | }, { 37 | "path" : "2", 38 | "coordinateTransformations" : [ { 39 | "scale" : [ 1.0, 1.0, 1.0, 4.0, 4.0 ], 40 | "type" : "scale" 41 | } ] 42 | }, { 43 | "path" : "3", 44 | "coordinateTransformations" : [ { 45 | "scale" : [ 1.0, 1.0, 1.0, 8.0, 8.0 ], 46 | "type" : "scale" 47 | } ] 48 | } ], 49 | "version" : "0.4" 50 | } ], 51 | "omero" : { 52 | "channels" : [ { 53 | "color" : "FF0000", 54 | "coefficient" : 1, 55 | "active" : true, 56 | "label" : "Channel 0", 57 | "window" : { 58 | "min" : 0.0, 59 | "max" : 255.0, 60 | "start" : 0.0, 61 | "end" : 255.0 62 | }, 63 | "family" : "linear", 64 | "inverted" : false 65 | }, { 66 | "color" : "00FF00", 67 | "coefficient" : 1, 68 | "active" : true, 69 | "label" : "Channel 1", 70 | "window" : { 71 | "min" : 0.0, 72 | "max" : 255.0, 73 | "start" : 0.0, 74 | "end" : 255.0 75 | }, 76 | "family" : "linear", 77 | "inverted" : false 78 | }, { 79 | "color" : "0000FF", 80 | "coefficient" : 1, 81 | "active" : true, 82 | "label" : "Channel 2", 83 | "window" : { 84 | "min" : 0.0, 85 | "max" : 255.0, 86 | "start" : 0.0, 87 | "end" : 255.0 88 | }, 89 | "family" : "linear", 90 | "inverted" : false 91 | } ], 92 | "rdefs" : { 93 | "defaultT" : 0, 94 | "model" : "color", 95 | "defaultZ" : 0 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format" : 2 3 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/0/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks" : [ 1, 1, 1, 431, 1024 ], 3 | "compressor" : { 4 | "clevel" : 5, 5 | "blocksize" : 0, 6 | "shuffle" : 1, 7 | "cname" : "lz4", 8 | "id" : "blosc" 9 | }, 10 | "dtype" : "|u1", 11 | "fill_value" : 0, 12 | "filters" : null, 13 | "order" : "C", 14 | "shape" : [ 1, 3, 1, 431, 1280 ], 15 | "dimension_separator" : "/", 16 | "zarr_format" : 2 17 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/0/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/0/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/0/0/0/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/0/0/0/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/1/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/1/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/1/0/0/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/1/0/0/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/2/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/2/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/2/0/0/1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/0/0/2/0/0/1 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/1/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks" : [ 1, 1, 1, 215, 640 ], 3 | "compressor" : { 4 | "clevel" : 5, 5 | "blocksize" : 0, 6 | "shuffle" : 1, 7 | "cname" : "lz4", 8 | "id" : "blosc" 9 | }, 10 | "dtype" : "|u1", 11 | "fill_value" : 0, 12 | "filters" : null, 13 | "order" : "C", 14 | "shape" : [ 1, 3, 1, 215, 640 ], 15 | "dimension_separator" : "/", 16 | "zarr_format" : 2 17 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/1/0/0/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/1/0/0/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/1/0/1/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/1/0/1/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/1/0/2/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/1/0/2/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/2/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks" : [ 1, 1, 1, 107, 320 ], 3 | "compressor" : { 4 | "clevel" : 5, 5 | "blocksize" : 0, 6 | "shuffle" : 1, 7 | "cname" : "lz4", 8 | "id" : "blosc" 9 | }, 10 | "dtype" : "|u1", 11 | "fill_value" : 0, 12 | "filters" : null, 13 | "order" : "C", 14 | "shape" : [ 1, 3, 1, 107, 320 ], 15 | "dimension_separator" : "/", 16 | "zarr_format" : 2 17 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/2/0/0/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/2/0/0/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/2/0/1/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/2/0/1/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/2/0/2/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/2/0/2/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/3/.zarray: -------------------------------------------------------------------------------- 1 | { 2 | "chunks" : [ 1, 1, 1, 53, 160 ], 3 | "compressor" : { 4 | "clevel" : 5, 5 | "blocksize" : 0, 6 | "shuffle" : 1, 7 | "cname" : "lz4", 8 | "id" : "blosc" 9 | }, 10 | "dtype" : "|u1", 11 | "fill_value" : 0, 12 | "filters" : null, 13 | "order" : "C", 14 | "shape" : [ 1, 3, 1, 53, 160 ], 15 | "dimension_separator" : "/", 16 | "zarr_format" : 2 17 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/3/0/0/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/3/0/0/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/3/0/1/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/3/0/1/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/2/3/0/2/0/0/0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.ome.zarr/2/3/0/2/0/0/0 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/OME/.zattrs: -------------------------------------------------------------------------------- 1 | { 2 | "series" : [ "0", "1", "2" ] 3 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/OME/.zgroup: -------------------------------------------------------------------------------- 1 | { 2 | "zarr_format" : 2 3 | } -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.ome.zarr/OME/METADATA.ome.xml: -------------------------------------------------------------------------------- 1 | 2009-12-29T09:59:15Aperio Image Library v11.2.1 2009-12-29T09:59:15label 387x4632009-12-29T09:59:15macro 1280x431Series 0 Top23.449873Series 0 Originalheight33014Series 0 LineAreaXOffset0.019265PlanarConfigurationChunkyCommentmacro 1280x431Series 0 ScanScope IDCPAPERIOCSBitsPerSample8MetaDataPhotometricInterpretationRGBNewSubfileType0Series 0 46000x32914 [42673,5576 2220x2967] (240x240) JPEG/RGB Q30;Aperio Image Library v10.0.51Series 0 AppMag20TileByteCounts3684Series 0 Left25.691574PhotometricInterpretationRGBSeries 0 LineAreaYOffset-0.000313Series 0 OriginalHeight32914TileOffsets16ImageLength2967Series 0 Time09:59:15ImageWidth2220SamplesPerPixel3Series 0 46920x33014 [0,100 46000x32914] (256x256) JPEG/RGB Q30TileLength240Series 0 LineCameraSkew-0.000424Series 0 ParmsetUSM FilterSeries 0 FilenameCMU-1Series 0 Date12/29/09Series 0 ImageID1004486Series 0 OriginalWidth46000Series 0 Focus Offset0.000000Series 0 Filtered5Series 0 MPP0.4990YCbCrSubSamplingchroma image dimensions are half the luma image dimensionsCompressionJPEGTileWidth240Series 0 Userb414003d-95c6-48b0-9369-8010ed517ba7NumberOfChannels3Series 0 StripeWidth2040574 768 -------------------------------------------------------------------------------- /tests/data/CMU-1-Small-Region.svs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/CMU-1-Small-Region.svs -------------------------------------------------------------------------------- /tests/data/UTM2GTIF.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/UTM2GTIF.tiff -------------------------------------------------------------------------------- /tests/data/artificial-ome-tiff/4D-series.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/artificial-ome-tiff/4D-series.ome.tif -------------------------------------------------------------------------------- /tests/data/artificial-ome-tiff/multi-channel-4D-series.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/artificial-ome-tiff/multi-channel-4D-series.ome.tif -------------------------------------------------------------------------------- /tests/data/artificial-ome-tiff/multi-channel-time-series.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/artificial-ome-tiff/multi-channel-time-series.ome.tif -------------------------------------------------------------------------------- /tests/data/artificial-ome-tiff/multi-channel-z-series.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/artificial-ome-tiff/multi-channel-z-series.ome.tif -------------------------------------------------------------------------------- /tests/data/artificial-ome-tiff/multi-channel.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/artificial-ome-tiff/multi-channel.ome.tif -------------------------------------------------------------------------------- /tests/data/artificial-ome-tiff/single-channel.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/artificial-ome-tiff/single-channel.ome.tif -------------------------------------------------------------------------------- /tests/data/artificial-ome-tiff/time-series.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/artificial-ome-tiff/time-series.ome.tif -------------------------------------------------------------------------------- /tests/data/artificial-ome-tiff/z-series.ome.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/artificial-ome-tiff/z-series.ome.tif -------------------------------------------------------------------------------- /tests/data/pngs/PNG_1_L.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/pngs/PNG_1_L.png -------------------------------------------------------------------------------- /tests/data/pngs/PNG_2_RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/pngs/PNG_2_RGB.png -------------------------------------------------------------------------------- /tests/data/pngs/PNG_2_RGBA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/pngs/PNG_2_RGBA.png -------------------------------------------------------------------------------- /tests/data/rand_uint16.ome.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/data/rand_uint16.ome.tiff -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/converters/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/integration/converters/test_ome_tiff_experimental.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import tifffile 4 | 5 | import tiledb 6 | from tests import assert_image_similarity, get_path 7 | from tiledb.bioimg.converters.ome_tiff import OMETiffConverter 8 | from tiledb.bioimg.helpers import open_bioimg 9 | from tiledb.bioimg.openslide import TileDBOpenSlide 10 | from tiledb.filter import WebpFilter 11 | 12 | 13 | # We need to expand on the test files. Most of the test files we have currently are not memory 14 | # contiguous and the ones that are not RGB files to test the different compressors 15 | @pytest.mark.parametrize("filename,num_series", [("UTM2GTIF.tiff", 1)]) 16 | @pytest.mark.parametrize("preserve_axes", [False, True]) 17 | @pytest.mark.parametrize("chunked,max_workers", [(False, 0), (True, 0), (True, 4)]) 18 | @pytest.mark.parametrize( 19 | "compressor", 20 | [tiledb.ZstdFilter(level=0)], 21 | ) 22 | def test_ome_tiff_converter_exclude_original_metadata( 23 | tmp_path, filename, num_series, preserve_axes, chunked, max_workers, compressor 24 | ): 25 | # The image is not RGB to use the WbeP compressor 26 | if isinstance(compressor, tiledb.WebpFilter) and filename == "UTM2GTIF.tiff": 27 | pytest.skip(f"WebPFilter cannot be applied to {filename}") 28 | 29 | input_path = get_path(filename) 30 | tiledb_path = tmp_path / "to_tiledb" 31 | exprimental_path = tmp_path / "experimental" 32 | OMETiffConverter.to_tiledb( 33 | input_path, 34 | str(tiledb_path), 35 | preserve_axes=preserve_axes, 36 | chunked=chunked, 37 | max_workers=max_workers, 38 | compressor=compressor, 39 | log=False, 40 | exclude_metadata=True, 41 | ) 42 | 43 | OMETiffConverter.to_tiledb( 44 | input_path, 45 | str(exprimental_path), 46 | preserve_axes=preserve_axes, 47 | chunked=True, 48 | max_workers=max_workers, 49 | compressor=compressor, 50 | log=False, 51 | exclude_metadata=True, 52 | experimental_reader=True, 53 | ) 54 | 55 | with TileDBOpenSlide(str(tiledb_path)) as t: 56 | with TileDBOpenSlide(str(exprimental_path)) as e: 57 | assert t.level_count == e.level_count 58 | 59 | for level in range(t.level_count): 60 | np.testing.assert_array_equal(t.read_level(level), e.read_level(level)) 61 | 62 | 63 | @pytest.mark.parametrize("filename,num_series", [("UTM2GTIF.tiff", 1)]) 64 | @pytest.mark.parametrize("preserve_axes", [False, True]) 65 | @pytest.mark.parametrize("chunked,max_workers", [(True, 0), (True, 4)]) 66 | @pytest.mark.parametrize( 67 | "compressor", 68 | [tiledb.ZstdFilter(level=0)], 69 | ) 70 | def test_ome_tiff_converter_roundtrip( 71 | tmp_path, filename, num_series, preserve_axes, chunked, max_workers, compressor 72 | ): 73 | if isinstance(compressor, tiledb.WebpFilter) and filename == "UTM2GTIF.tiff": 74 | pytest.skip(f"WebPFilter cannot be applied to {filename}") 75 | 76 | input_path = get_path(filename) 77 | tiledb_path = tmp_path / "to_tiledb" 78 | output_path = tmp_path / "from_tiledb" 79 | OMETiffConverter.to_tiledb( 80 | input_path, 81 | str(tiledb_path), 82 | preserve_axes=preserve_axes, 83 | chunked=chunked, 84 | max_workers=max_workers, 85 | compressor=compressor, 86 | log=False, 87 | experimental_reader=True, 88 | reader_kwargs=dict( 89 | extra_tags=( 90 | "ModelPixelScaleTag", 91 | "ModelTiepointTag", 92 | "GeoKeyDirectoryTag", 93 | "GeoAsciiParamsTag", 94 | ) 95 | ), 96 | ) 97 | # Store it back to NGFF Zarr 98 | OMETiffConverter.from_tiledb(str(tiledb_path), str(output_path)) 99 | 100 | with tifffile.TiffFile(input_path) as t1, tifffile.TiffFile(output_path) as t2: 101 | compare_tiff(t1, t2, lossless=False) 102 | 103 | 104 | @pytest.mark.parametrize( 105 | "filename,dims", 106 | [ 107 | ("single-channel.ome.tif", "YX"), 108 | ("z-series.ome.tif", "ZYX"), 109 | ("multi-channel.ome.tif", "CYX"), 110 | ("time-series.ome.tif", "TYX"), 111 | ("multi-channel-z-series.ome.tif", "CZYX"), 112 | ("multi-channel-time-series.ome.tif", "TCYX"), 113 | ("4D-series.ome.tif", "TZYX"), 114 | ("multi-channel-4D-series.ome.tif", "TCZYX"), 115 | ], 116 | ) 117 | @pytest.mark.parametrize("tiles", [{}, {"X": 128, "Y": 128, "Z": 2, "C": 1, "T": 3}]) 118 | def test_ome_tiff_converter_artificial_rountrip(tmp_path, filename, dims, tiles): 119 | input_path = get_path(f"artificial-ome-tiff/{filename}") 120 | tiledb_path = tmp_path / "to_tiledb" 121 | experimental_path = tmp_path / "_experimental" 122 | output_path = tmp_path / "from_tiledb" 123 | 124 | OMETiffConverter.to_tiledb(input_path, str(tiledb_path), tiles=tiles) 125 | OMETiffConverter.to_tiledb( 126 | input_path, 127 | str(experimental_path), 128 | tiles=tiles, 129 | experimental_reader=True, 130 | chunked=True, 131 | max_workers=16, 132 | ) 133 | 134 | with TileDBOpenSlide(str(experimental_path)) as t: 135 | assert len(tiledb.Group(str(experimental_path))) == t.level_count == 1 136 | 137 | with open_bioimg(str(experimental_path / "l_0.tdb")) as A: 138 | assert "".join(dim.name for dim in A.domain) == dims 139 | assert A.dtype == np.int8 140 | assert A.dim("X").tile == tiles.get("X", 439) 141 | assert A.dim("Y").tile == tiles.get("Y", 167) 142 | if A.domain.has_dim("Z"): 143 | assert A.dim("Z").tile == tiles.get("Z", 1) 144 | if A.domain.has_dim("C"): 145 | assert A.dim("C").tile == tiles.get("C", 3) 146 | if A.domain.has_dim("T"): 147 | assert A.dim("T").tile == tiles.get("T", 1) 148 | 149 | OMETiffConverter.from_tiledb(str(tiledb_path), str(output_path)) 150 | with tifffile.TiffFile(input_path) as t1, tifffile.TiffFile(output_path) as t2: 151 | compare_tiff(t1, t2, lossless=True) 152 | 153 | 154 | def compare_tiff(t1: tifffile.TiffFile, t2: tifffile.TiffFile, lossless: bool = True): 155 | assert len(t1.series[0].levels) == len(t2.series[0].levels) 156 | 157 | for l1, l2 in zip(t1.series[0].levels, t2.series[0].levels): 158 | assert l1.axes.replace("S", "C") == l2.axes.replace("S", "C") 159 | assert l1.shape == l2.shape 160 | assert l1.dtype == l2.dtype 161 | assert l1.nbytes == l2.nbytes 162 | 163 | if lossless: 164 | np.testing.assert_array_equal(l1.asarray(), l2.asarray()) 165 | else: 166 | assert_image_similarity(l1.asarray(), l2.asarray(), channel_axis=0) 167 | 168 | 169 | compressors = [ 170 | None, 171 | tiledb.ZstdFilter(level=0), 172 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGB, lossless=False), 173 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGB, lossless=True), 174 | ] 175 | -------------------------------------------------------------------------------- /tests/integration/converters/test_ome_zarr.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import numpy as np 4 | import PIL.Image 5 | import pytest 6 | import zarr 7 | 8 | import tiledb 9 | from tests import assert_image_similarity, get_path, get_schema 10 | from tiledb.bioimg.converters import DATASET_TYPE, FMT_VERSION 11 | from tiledb.bioimg.converters.ome_zarr import OMEZarrConverter 12 | from tiledb.bioimg.helpers import iter_color, open_bioimg 13 | from tiledb.bioimg.openslide import TileDBOpenSlide 14 | from tiledb.filter import WebpFilter 15 | 16 | schemas = (get_schema(2220, 2967), get_schema(387, 463), get_schema(1280, 431)) 17 | 18 | 19 | @pytest.mark.parametrize("series_idx", [0, 1, 2]) 20 | @pytest.mark.parametrize("preserve_axes", [False, True]) 21 | def test_ome_zarr_converter_source_reader_exception( 22 | tmp_path, series_idx, preserve_axes 23 | ): 24 | tiff_path = get_path("CMU-1-Small-Region.ome.tiff") 25 | output_reader = tmp_path / "to_tiledb_reader" 26 | 27 | with pytest.raises(FileExistsError) as excinfo: 28 | OMEZarrConverter.to_tiledb( 29 | tiff_path, str(output_reader), preserve_axes=preserve_axes 30 | ) 31 | assert "FileExistsError" in str(excinfo) 32 | 33 | 34 | @pytest.mark.parametrize("series_idx", [0, 1, 2]) 35 | @pytest.mark.parametrize("preserve_axes", [False, True]) 36 | def test_ome_zarr_converter_reader_source_consistent_output( 37 | tmp_path, series_idx, preserve_axes 38 | ): 39 | input_path = get_path("CMU-1-Small-Region.ome.zarr") / str(series_idx) 40 | 41 | output_path = tmp_path / "to_tiledb_path" 42 | output_reader = tmp_path / "to_tiledb_reader" 43 | 44 | OMEZarrConverter.to_tiledb( 45 | input_path, str(output_path), preserve_axes=preserve_axes 46 | ) 47 | OMEZarrConverter.to_tiledb( 48 | input_path, str(output_reader), preserve_axes=preserve_axes 49 | ) 50 | 51 | # check the first (highest) resolution layer only 52 | schema = schemas[series_idx] 53 | with open_bioimg(str(output_path / "l_0.tdb")) as A: 54 | with open_bioimg(str(output_reader / "l_0.tdb")) as B: 55 | if not preserve_axes: 56 | assert schema == A.schema == B.schema 57 | else: 58 | assert A.schema == B.schema 59 | 60 | 61 | @pytest.mark.parametrize("series_idx", [0, 1, 2]) 62 | @pytest.mark.parametrize("preserve_axes", [False, True]) 63 | def test_ome_zarr_converter(tmp_path, series_idx, preserve_axes): 64 | input_path = get_path("CMU-1-Small-Region.ome.zarr") / str(series_idx) 65 | OMEZarrConverter.to_tiledb(input_path, str(tmp_path), preserve_axes=preserve_axes) 66 | 67 | # check the first (highest) resolution layer only 68 | schema = schemas[series_idx] 69 | with open_bioimg(str(tmp_path / "l_0.tdb")) as A: 70 | if not preserve_axes: 71 | assert A.schema == schema 72 | 73 | with TileDBOpenSlide(str(tmp_path)) as t: 74 | assert t.dimensions == t.level_dimensions[0] == schema.shape[:-3:-1] 75 | 76 | region_location = (100, 100) 77 | region_size = (300, 400) 78 | region = t.read_region(level=0, location=region_location, size=region_size) 79 | assert isinstance(region, np.ndarray) 80 | assert region.ndim == 3 81 | assert region.dtype == np.uint8 82 | img = PIL.Image.fromarray(region) 83 | assert img.size == ( 84 | min(t.dimensions[0] - region_location[0], region_size[0]), 85 | min(t.dimensions[1] - region_location[1], region_size[1]), 86 | ) 87 | 88 | for level in range(t.level_count): 89 | region_data = t.read_region((0, 0), level, t.level_dimensions[level]) 90 | level_data = t.read_level(level) 91 | np.testing.assert_array_equal(region_data, level_data) 92 | 93 | 94 | @pytest.mark.parametrize("series_idx", [0, 1, 2]) 95 | @pytest.mark.parametrize("preserve_axes", [False, True]) 96 | @pytest.mark.parametrize("chunked,max_workers", [(False, 0), (True, 0), (True, 4)]) 97 | @pytest.mark.parametrize( 98 | "compressor", 99 | [ 100 | tiledb.ZstdFilter(level=0), 101 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGB, lossless=False), 102 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGB, lossless=True), 103 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_NONE, lossless=True), 104 | ], 105 | ) 106 | def test_ome_zarr_converter_rountrip( 107 | tmp_path, series_idx, preserve_axes, chunked, max_workers, compressor 108 | ): 109 | input_path = get_path("CMU-1-Small-Region.ome.zarr") / str(series_idx) 110 | tiledb_path = tmp_path / "to_tiledb" 111 | output_path = tmp_path / "from_tiledb" 112 | OMEZarrConverter.to_tiledb( 113 | input_path, 114 | str(tiledb_path), 115 | preserve_axes=preserve_axes, 116 | chunked=chunked, 117 | max_workers=max_workers, 118 | compressor=compressor, 119 | ) 120 | # Store it back to NGFF Zarr 121 | OMEZarrConverter.from_tiledb(str(tiledb_path), str(output_path)) 122 | 123 | # Same number of levels 124 | input_group = zarr.open_group(input_path, mode="r") 125 | tiledb_group = tiledb.Group(str(tiledb_path), mode="r") 126 | output_group = zarr.open_group(output_path, mode="r") 127 | assert len(input_group) == len(tiledb_group) 128 | assert len(input_group) == len(output_group) 129 | 130 | # Compare the .zattrs files 131 | with open(input_path / ".zattrs") as f: 132 | input_attrs = json.load(f) 133 | # ome-zarr-py replaces empty name with "/" 134 | name = input_attrs["multiscales"][0]["name"] 135 | if not name: 136 | input_attrs["multiscales"][0]["name"] = "/" 137 | with open(output_path / ".zattrs") as f: 138 | output_attrs = json.load(f) 139 | assert input_attrs == output_attrs 140 | 141 | # Compare the level arrays 142 | for i in range(len(input_group)): 143 | # Compare the .zarray files 144 | with open(input_path / str(i) / ".zarray") as f: 145 | input_zarray = json.load(f) 146 | with open(output_path / str(i) / ".zarray") as f: 147 | output_zarray = json.load(f) 148 | assert input_zarray == output_zarray 149 | 150 | # Compare the actual data 151 | input_array = zarr.open(input_path / str(i))[:] 152 | output_array = zarr.open(output_path / str(i))[:] 153 | if isinstance(compressor, tiledb.WebpFilter) and not compressor.lossless: 154 | assert_image_similarity( 155 | input_array.squeeze(), 156 | output_array.squeeze(), 157 | channel_axis=0, 158 | min_threshold=0.87, 159 | ) 160 | else: 161 | np.testing.assert_array_equal(input_array, output_array) 162 | 163 | 164 | def test_ome_zarr_converter_incremental(tmp_path): 165 | input_path = get_path("CMU-1-Small-Region.ome.zarr/0") 166 | 167 | OMEZarrConverter.to_tiledb(input_path, str(tmp_path), level_min=1) 168 | with TileDBOpenSlide(str(tmp_path)) as t: 169 | assert len(tiledb.Group(str(tmp_path))) == t.level_count == 1 170 | 171 | OMEZarrConverter.to_tiledb(input_path, str(tmp_path), level_min=0) 172 | with TileDBOpenSlide(str(tmp_path)) as t: 173 | assert len(tiledb.Group(str(tmp_path))) == t.level_count == 2 174 | 175 | OMEZarrConverter.to_tiledb(input_path, str(tmp_path), level_min=0) 176 | with TileDBOpenSlide(str(tmp_path)) as t: 177 | assert len(tiledb.Group(str(tmp_path))) == t.level_count == 2 178 | 179 | 180 | @pytest.mark.parametrize("series_idx", [0, 1, 2]) 181 | @pytest.mark.parametrize("preserve_axes", [False, True]) 182 | def test_ome_zarr_converter_group_meta(tmp_path, series_idx, preserve_axes): 183 | input_path = get_path("CMU-1-Small-Region.ome.zarr") / str(series_idx) 184 | OMEZarrConverter.to_tiledb(input_path, str(tmp_path), preserve_axes=preserve_axes) 185 | 186 | with TileDBOpenSlide(str(tmp_path)) as t: 187 | group_properties = t.properties 188 | assert group_properties["dataset_type"] == DATASET_TYPE 189 | assert group_properties["fmt_version"] == FMT_VERSION 190 | assert isinstance(group_properties.get("pkg_version"), str) 191 | assert group_properties["axes"] == "TCZYX" 192 | assert group_properties["channels"] == json.dumps( 193 | ["Channel 0", "Channel 1", "Channel 2"] 194 | ) 195 | 196 | levels_group_meta = json.loads(group_properties["levels"]) 197 | assert t.level_count == len(levels_group_meta) 198 | for level, level_meta in enumerate(levels_group_meta): 199 | assert level_meta["level"] == level 200 | assert level_meta["name"] == f"l_{level}.tdb" 201 | 202 | level_axes = level_meta["axes"] 203 | shape = level_meta["shape"] 204 | level_width, level_height = t.level_dimensions[level] 205 | assert level_axes == "TCZYX" if preserve_axes else "CYX" 206 | assert len(shape) == len(level_axes) 207 | assert shape[level_axes.index("C")] == 3 208 | assert shape[level_axes.index("X")] == level_width 209 | assert shape[level_axes.index("Y")] == level_height 210 | if preserve_axes: 211 | assert shape[level_axes.index("T")] == 1 212 | assert shape[level_axes.index("Z")] == 1 213 | 214 | 215 | @pytest.mark.parametrize("series_idx", [0, 1, 2]) 216 | @pytest.mark.parametrize("preserve_axes", [False, True]) 217 | def test_ome_zarr_converter_channel_meta(tmp_path, series_idx, preserve_axes): 218 | input_path = get_path("CMU-1-Small-Region.ome.zarr") / str(series_idx) 219 | OMEZarrConverter.to_tiledb(input_path, str(tmp_path), preserve_axes=preserve_axes) 220 | 221 | with TileDBOpenSlide(str(tmp_path)) as t: 222 | group_properties = t.properties 223 | assert "metadata" in group_properties 224 | 225 | image_meta = json.loads(group_properties["metadata"]) 226 | color_generator = iter_color(np.dtype(np.uint8)) 227 | 228 | assert len(image_meta["channels"]) == 1 229 | assert "intensity" in image_meta["channels"] 230 | assert len(image_meta["channels"]["intensity"]) == 3 231 | for channel in image_meta["channels"]["intensity"]: 232 | assert channel["color"] == next(color_generator) 233 | assert channel["min"] == 0 234 | assert channel["max"] == 255 235 | -------------------------------------------------------------------------------- /tests/integration/converters/test_openslide.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import numpy as np 4 | import PIL.Image 5 | import pytest 6 | 7 | import tiledb 8 | from tests import assert_image_similarity, get_path, get_schema 9 | from tiledb.bioimg.converters import DATASET_TYPE, FMT_VERSION 10 | from tiledb.bioimg.converters.openslide import OpenSlideConverter 11 | from tiledb.bioimg.helpers import open_bioimg 12 | from tiledb.bioimg.openslide import TileDBOpenSlide 13 | from tiledb.filter import WebpFilter 14 | 15 | 16 | @pytest.mark.parametrize("preserve_axes", [False, True]) 17 | @pytest.mark.parametrize("chunked,max_workers", [(False, 0), (True, 0), (True, 4)]) 18 | @pytest.mark.parametrize( 19 | "compressor", 20 | [ 21 | tiledb.ZstdFilter(level=0), 22 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGBA, lossless=False), 23 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGBA, lossless=True), 24 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_NONE, lossless=True), 25 | ], 26 | ) 27 | def test_openslide_converter(tmp_path, preserve_axes, chunked, max_workers, compressor): 28 | input_path = str(get_path("CMU-1-Small-Region.svs")) 29 | output_path = str(tmp_path) 30 | OpenSlideConverter.to_tiledb( 31 | input_path, 32 | output_path, 33 | preserve_axes=preserve_axes, 34 | chunked=chunked, 35 | max_workers=max_workers, 36 | compressor=compressor, 37 | ) 38 | assert len(tiledb.Group(output_path)) == 1 39 | with open_bioimg(str(tmp_path / "l_0.tdb")) as A: 40 | if not preserve_axes: 41 | assert A.schema == get_schema(2220, 2967, 4, compressor=compressor) 42 | 43 | import openslide 44 | 45 | o = openslide.open_slide(input_path) 46 | with TileDBOpenSlide(output_path) as t: 47 | group_properties = t.properties 48 | assert group_properties["dataset_type"] == DATASET_TYPE 49 | assert group_properties["fmt_version"] == FMT_VERSION 50 | assert isinstance(group_properties.get("pkg_version"), str) 51 | assert group_properties["axes"] == "YXC" 52 | assert group_properties["channels"] == json.dumps( 53 | ["RED", "GREEN", "BLUE", "ALPHA"] 54 | ) 55 | levels_group_meta = json.loads(group_properties["levels"]) 56 | assert t.level_count == len(levels_group_meta) 57 | 58 | assert t.level_count == o.level_count 59 | assert t.dimensions == o.dimensions 60 | assert t.level_dimensions == o.level_dimensions 61 | assert t.level_downsamples == o.level_downsamples 62 | 63 | region_kwargs = dict(level=0, location=(123, 234), size=(1328, 1896)) 64 | 65 | region = t.read_region(**region_kwargs) 66 | assert isinstance(region, np.ndarray) 67 | assert region.ndim == 3 68 | assert region.dtype == np.uint8 69 | t_img = PIL.Image.fromarray(region) 70 | o_img = o.read_region(**region_kwargs) 71 | if isinstance(compressor, tiledb.WebpFilter) and not compressor.lossless: 72 | assert_image_similarity(np.asarray(t_img), np.asarray(o_img)) 73 | else: 74 | assert t_img == o_img 75 | 76 | for level in range(t.level_count): 77 | region_data = t.read_region((0, 0), level, t.level_dimensions[level]) 78 | level_data = t.read_level(level) 79 | np.testing.assert_array_equal(region_data, level_data) 80 | 81 | 82 | @pytest.mark.parametrize("preserve_axes", [False, True]) 83 | def test_openslide_converter_group_metadata(tmp_path, preserve_axes): 84 | input_path = str(get_path("CMU-1-Small-Region.svs")) 85 | output_path = str(tmp_path) 86 | OpenSlideConverter.to_tiledb(input_path, output_path, preserve_axes=preserve_axes) 87 | 88 | with TileDBOpenSlide(output_path) as t: 89 | group_properties = t.properties 90 | assert group_properties["dataset_type"] == DATASET_TYPE 91 | assert group_properties["fmt_version"] == FMT_VERSION 92 | assert isinstance(group_properties.get("pkg_version"), str) 93 | assert group_properties["axes"] == "YXC" 94 | assert group_properties["channels"] == json.dumps( 95 | ["RED", "GREEN", "BLUE", "ALPHA"] 96 | ) 97 | 98 | levels_group_meta = json.loads(group_properties["levels"]) 99 | assert t.level_count == len(levels_group_meta) 100 | for level, level_meta in enumerate(levels_group_meta): 101 | assert level_meta["level"] == level 102 | assert level_meta["name"] == f"l_{level}.tdb" 103 | 104 | level_axes = level_meta["axes"] 105 | shape = level_meta["shape"] 106 | level_width, level_height = t.level_dimensions[level] 107 | assert level_axes == "YXC" if preserve_axes else "CYX" 108 | assert len(shape) == len(level_axes) 109 | assert shape[level_axes.index("C")] == 4 110 | assert shape[level_axes.index("X")] == level_width 111 | assert shape[level_axes.index("Y")] == level_height 112 | -------------------------------------------------------------------------------- /tests/integration/converters/test_png.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import numpy as np 4 | import pytest 5 | from PIL import Image, ImageChops 6 | 7 | import tiledb 8 | from tests import assert_image_similarity, get_path, get_schema 9 | from tiledb.bioimg.converters import DATASET_TYPE, FMT_VERSION 10 | from tiledb.bioimg.converters.png import PNGConverter 11 | from tiledb.bioimg.helpers import open_bioimg 12 | from tiledb.bioimg.openslide import TileDBOpenSlide 13 | from tiledb.filter import WebpFilter 14 | 15 | 16 | def create_synthetic_image( 17 | mode="RGB", width=100, height=100, filename="synthetic_image.png" 18 | ): 19 | """ 20 | Creates a synthetic image with either RGB or RGBA channels and saves it as a PNG file. 21 | 22 | Parameters: 23 | - image_type: 'RGB' for 3 channels, 'RGBA' for 4 channels. 24 | - width: width of the image. 25 | - height: height of the image. 26 | - filename: filename to store the image as a PNG. 27 | """ 28 | if mode == "RGB": 29 | # Create a (height, width, 3) NumPy array with random values for RGB 30 | data = np.random.randint(0, 256, (height, width, 3), dtype=np.uint8) 31 | elif mode == "RGBA": 32 | # Create a (height, width, 4) NumPy array with random values for RGBA 33 | data = np.random.randint(0, 256, (height, width, 4), dtype=np.uint8) 34 | else: 35 | raise ValueError("Other image type are tested with sample images.") 36 | # Convert NumPy array to a Pillow Image 37 | image = Image.fromarray(data, mode) 38 | # Save the image as a PNG 39 | image.save(filename) 40 | return filename 41 | 42 | 43 | def test_png_converter(tmp_path): 44 | input_path = str(get_path("pngs/PNG_1_L.png")) 45 | output_path = str(tmp_path) 46 | 47 | PNGConverter.to_tiledb(input_path, output_path) 48 | 49 | with TileDBOpenSlide(output_path) as t: 50 | assert len(tiledb.Group(output_path)) == t.level_count == 1 51 | schemas = get_schema(1080, 1080, c_size=1) 52 | # Storing the images as 3-channel images in CYX format 53 | # the slicing below using negative indexes to extract 54 | # the last two elements in schema's shape. 55 | assert t.dimensions == schemas.shape[:-3:-1] 56 | for i in range(t.level_count): 57 | assert t.level_dimensions[i] == schemas.shape[:-3:-1] 58 | with open_bioimg(str(tmp_path / f"l_{i}.tdb")) as A: 59 | assert A.schema == schemas 60 | 61 | region = t.read_region(level=0, location=(100, 100), size=(300, 400)) 62 | assert isinstance(region, np.ndarray) 63 | assert region.ndim == 3 64 | assert region.dtype == np.uint8 65 | img = Image.fromarray(region.squeeze()) 66 | assert img.size == (300, 400) 67 | 68 | for level in range(t.level_count): 69 | region_data = t.read_region((0, 0), level, t.level_dimensions[level]) 70 | level_data = t.read_level(level) 71 | np.testing.assert_array_equal(region_data, level_data) 72 | 73 | 74 | @pytest.mark.parametrize("filename", ["pngs/PNG_1_L.png"]) 75 | def test_png_converter_group_metadata(tmp_path, filename): 76 | input_path = get_path(filename) 77 | tiledb_path = str(tmp_path / "to_tiledb") 78 | PNGConverter.to_tiledb(input_path, tiledb_path, preserve_axes=False) 79 | 80 | with TileDBOpenSlide(tiledb_path) as t: 81 | group_properties = t.properties 82 | assert group_properties["dataset_type"] == DATASET_TYPE 83 | assert group_properties["fmt_version"] == FMT_VERSION 84 | assert isinstance(group_properties["pkg_version"], str) 85 | assert group_properties["axes"] == "XY" 86 | assert group_properties["channels"] == json.dumps(["GRAYSCALE"]) 87 | 88 | levels_group_meta = json.loads(group_properties["levels"]) 89 | assert t.level_count == len(levels_group_meta) 90 | for level, level_meta in enumerate(levels_group_meta): 91 | assert level_meta["level"] == level 92 | assert level_meta["name"] == f"l_{level}.tdb" 93 | 94 | level_axes = level_meta["axes"] 95 | shape = level_meta["shape"] 96 | level_width, level_height = t.level_dimensions[level] 97 | assert level_axes == "YX" 98 | assert len(shape) == len(level_axes) 99 | assert shape[level_axes.index("X")] == level_width 100 | assert shape[level_axes.index("Y")] == level_height 101 | 102 | 103 | def compare_png(p1: Image, p2: Image, lossless: bool = True): 104 | if lossless: 105 | diff = ImageChops.difference(p1, p2) 106 | assert diff.getbbox() is None 107 | else: 108 | try: 109 | # Default min_threshold is 0.95 110 | assert_image_similarity(np.array(p1), np.array(p2), channel_axis=-1) 111 | except AssertionError: 112 | try: 113 | # for PNGs the min_threshold for WEBP lossy is < 0.85 114 | assert_image_similarity( 115 | np.array(p1), np.array(p2), min_threshold=0.84, channel_axis=-1 116 | ) 117 | except AssertionError: 118 | assert False 119 | 120 | 121 | # PIL.Image does not support chunked reads/writes 122 | @pytest.mark.parametrize("preserve_axes", [False, True]) 123 | @pytest.mark.parametrize("chunked", [False]) 124 | @pytest.mark.parametrize( 125 | "compressor, lossless", 126 | [ 127 | (tiledb.ZstdFilter(level=0), True), 128 | (tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGB, lossless=True), True), 129 | (tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_NONE, lossless=True), True), 130 | ], 131 | ) 132 | @pytest.mark.parametrize( 133 | "mode, width, height", 134 | [ 135 | ("RGB", 200, 200), # Square RGB image 136 | ("RGB", 150, 100), # Uneven dimensions 137 | ("RGB", 50, 150), # Tall image 138 | ], 139 | ) 140 | def test_png_converter_RGB_roundtrip( 141 | tmp_path, preserve_axes, chunked, compressor, lossless, mode, width, height 142 | ): 143 | 144 | input_path = str(tmp_path / f"test_{mode.lower()}_image_{width}x{height}.png") 145 | # Call the function to create a synthetic image 146 | create_synthetic_image(mode=mode, width=width, height=height, filename=input_path) 147 | tiledb_path = str(tmp_path / "to_tiledb") 148 | output_path = str(tmp_path / "from_tiledb") 149 | PNGConverter.to_tiledb( 150 | input_path, 151 | tiledb_path, 152 | preserve_axes=preserve_axes, 153 | chunked=chunked, 154 | compressor=compressor, 155 | log=False, 156 | ) 157 | # Store it back to PNG 158 | PNGConverter.from_tiledb(tiledb_path, output_path) 159 | compare_png(Image.open(input_path), Image.open(output_path), lossless=lossless) 160 | 161 | 162 | @pytest.mark.parametrize("filename", ["pngs/PNG_1_L.png"]) 163 | @pytest.mark.parametrize("preserve_axes", [False, True]) 164 | @pytest.mark.parametrize("chunked", [False]) 165 | @pytest.mark.parametrize( 166 | "compressor, lossless", 167 | [ 168 | (tiledb.ZstdFilter(level=0), True), 169 | # WEBP is not supported for Grayscale images 170 | ], 171 | ) 172 | def test_png_converter_L_roundtrip( 173 | tmp_path, preserve_axes, chunked, compressor, lossless, filename 174 | ): 175 | # For lossy WEBP we cannot use random generated images as they have so much noise 176 | input_path = str(get_path(filename)) 177 | tiledb_path = str(tmp_path / "to_tiledb") 178 | output_path = str(tmp_path / "from_tiledb") 179 | 180 | PNGConverter.to_tiledb( 181 | input_path, 182 | tiledb_path, 183 | preserve_axes=preserve_axes, 184 | chunked=chunked, 185 | compressor=compressor, 186 | log=False, 187 | ) 188 | # Store it back to PNG 189 | PNGConverter.from_tiledb(tiledb_path, output_path) 190 | compare_png(Image.open(input_path), Image.open(output_path), lossless=lossless) 191 | 192 | 193 | @pytest.mark.parametrize("filename", ["pngs/PNG_2_RGB.png"]) 194 | @pytest.mark.parametrize("preserve_axes", [False, True]) 195 | @pytest.mark.parametrize("chunked", [False]) 196 | @pytest.mark.parametrize( 197 | "compressor, lossless", 198 | [ 199 | (tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGB, lossless=False), False), 200 | ], 201 | ) 202 | def test_png_converter_RGB_roundtrip_lossy( 203 | tmp_path, preserve_axes, chunked, compressor, lossless, filename 204 | ): 205 | # For lossy WEBP we cannot use random generated images as they have so much noise 206 | input_path = str(get_path(filename)) 207 | tiledb_path = str(tmp_path / "to_tiledb") 208 | output_path = str(tmp_path / "from_tiledb") 209 | 210 | PNGConverter.to_tiledb( 211 | input_path, 212 | tiledb_path, 213 | preserve_axes=preserve_axes, 214 | chunked=chunked, 215 | compressor=compressor, 216 | log=False, 217 | ) 218 | # Store it back to PNG 219 | PNGConverter.from_tiledb(tiledb_path, output_path) 220 | compare_png(Image.open(input_path), Image.open(output_path), lossless=lossless) 221 | 222 | 223 | @pytest.mark.parametrize("preserve_axes", [False]) 224 | # PIL.Image does not support chunked reads/writes 225 | @pytest.mark.parametrize("chunked", [False]) 226 | @pytest.mark.parametrize( 227 | "mode, width, height", 228 | [ 229 | ("RGBA", 200, 200), # Square RGBA image 230 | ("RGBA", 300, 150), # Uneven dimensions 231 | ("RGBA", 120, 240), # Tall image 232 | ], 233 | ) 234 | @pytest.mark.parametrize( 235 | "compressor, lossless", 236 | [ 237 | (tiledb.ZstdFilter(level=0), True), 238 | (tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGBA, lossless=True), True), 239 | (tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_NONE, lossless=True), True), 240 | ], 241 | ) 242 | def test_png_converter_RGBA_roundtrip( 243 | tmp_path, preserve_axes, chunked, compressor, lossless, mode, width, height 244 | ): 245 | input_path = str(tmp_path / f"test_{mode.lower()}_image_{width}x{height}.png") 246 | # Call the function to create a synthetic image 247 | create_synthetic_image(mode=mode, width=width, height=height, filename=input_path) 248 | tiledb_path = str(tmp_path / "to_tiledb") 249 | output_path = str(tmp_path / "from_tiledb") 250 | PNGConverter.to_tiledb( 251 | input_path, 252 | tiledb_path, 253 | preserve_axes=preserve_axes, 254 | chunked=chunked, 255 | compressor=compressor, 256 | log=False, 257 | ) 258 | # Store it back to PNG 259 | PNGConverter.from_tiledb(tiledb_path, output_path) 260 | compare_png(Image.open(input_path), Image.open(output_path), lossless=lossless) 261 | 262 | 263 | @pytest.mark.parametrize("filename", ["pngs/PNG_2_RGBA.png"]) 264 | @pytest.mark.parametrize("preserve_axes", [False, True]) 265 | @pytest.mark.parametrize("chunked", [False]) 266 | @pytest.mark.parametrize( 267 | "compressor, lossless", 268 | [ 269 | ( 270 | tiledb.WebpFilter(WebpFilter.WebpInputFormat.WEBP_RGBA, lossless=False), 271 | False, 272 | ), 273 | ], 274 | ) 275 | def test_png_converter_RGBA_roundtrip_lossy( 276 | tmp_path, preserve_axes, chunked, compressor, lossless, filename 277 | ): 278 | # For lossy WEBP we cannot use random generated images as they have so much noise 279 | input_path = str(get_path(filename)) 280 | tiledb_path = str(tmp_path / "to_tiledb") 281 | output_path = str(tmp_path / "from_tiledb") 282 | 283 | PNGConverter.to_tiledb( 284 | input_path, 285 | tiledb_path, 286 | preserve_axes=preserve_axes, 287 | chunked=chunked, 288 | compressor=compressor, 289 | log=False, 290 | ) 291 | # Store it back to PNG 292 | PNGConverter.from_tiledb(tiledb_path, output_path) 293 | compare_png(Image.open(input_path), Image.open(output_path), lossless=lossless) 294 | -------------------------------------------------------------------------------- /tests/integration/converters/test_scaler.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests import assert_image_similarity, get_path 4 | from tiledb.bioimg.converters.ome_tiff import OMETiffConverter 5 | from tiledb.bioimg.openslide import TileDBOpenSlide 6 | 7 | 8 | @pytest.mark.parametrize("scale_factors", [[2, 4.0, 8, 16]]) 9 | @pytest.mark.parametrize("chunked,max_workers", [(False, 0), (True, 0), (True, 4)]) 10 | @pytest.mark.parametrize("progressive", [True, False]) 11 | def test_scaler(tmp_path, scale_factors, chunked, max_workers, progressive): 12 | input_path = str(get_path("CMU-1-Small-Region.ome.tiff")) 13 | ground_path = str(tmp_path / "ground") 14 | test_path = str(tmp_path / "test") 15 | 16 | OMETiffConverter.to_tiledb( 17 | input_path, 18 | ground_path, 19 | pyramid_kwargs={"scale_factors": scale_factors}, 20 | ) 21 | 22 | OMETiffConverter.to_tiledb( 23 | input_path, 24 | test_path, 25 | pyramid_kwargs={ 26 | "scale_factors": scale_factors, 27 | "chunked": chunked, 28 | "progressive": progressive, 29 | "order": 1, 30 | "max_workers": max_workers, 31 | }, 32 | ) 33 | 34 | with TileDBOpenSlide(ground_path) as ground, TileDBOpenSlide(test_path) as test: 35 | assert ground.level_count == test.level_count 36 | for level in range(ground.level_count): 37 | assert ground.level_dimensions[level] == test.level_dimensions[level] 38 | 39 | region_kwargs = dict( 40 | level=level, location=(0, 0), size=test.level_dimensions[level] 41 | ) 42 | ground_img = ground.read_region(**region_kwargs) 43 | test_img = test.read_region(**region_kwargs) 44 | assert_image_similarity(ground_img, test_img) 45 | -------------------------------------------------------------------------------- /tests/integration/test_wrappers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from tests import get_path 6 | from tiledb.bioimg import Converters, from_bioimg, to_bioimg 7 | from tiledb.bioimg.converters.ome_tiff import OMETiffConverter 8 | from tiledb.bioimg.converters.ome_zarr import OMEZarrConverter 9 | from tiledb.bioimg.converters.openslide import OpenSlideConverter 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "converter, file_path", 14 | [ 15 | (Converters.OMETIFF, "CMU-1-Small-Region-rgb.ome.tiff"), 16 | (Converters.OMETIFF, "CMU-1-Small-Region.ome.tiff"), 17 | (Converters.OMETIFF, "rand_uint16.ome.tiff"), 18 | (Converters.OMETIFF, "UTM2GTIF.tiff"), 19 | (Converters.OMEZARR, "CMU-1-Small-Region.ome.zarr"), 20 | (Converters.OSD, "CMU-1-Small-Region.svs"), 21 | ], 22 | ) 23 | def test_from_bioimg_wrapper(tmp_path, converter, file_path): 24 | input_path = str(get_path(file_path)) 25 | output_path = str(tmp_path) 26 | output_path_round = str(tmp_path) + "/roundtrip" 27 | if converter == Converters.OMETIFF: 28 | rfromtype = from_bioimg(input_path, output_path, converter=converter) 29 | rtotype = to_bioimg(output_path, output_path_round, converter=converter) 30 | assert rfromtype == rtotype == OMETiffConverter 31 | elif converter == Converters.OSD: 32 | rtype = from_bioimg(input_path, output_path, converter=converter) 33 | assert rtype == OpenSlideConverter 34 | with pytest.raises(NotImplementedError): 35 | to_bioimg(output_path, output_path_round, converter=converter) 36 | else: 37 | input_path = os.path.join(input_path, str(0)) 38 | rfromtype = from_bioimg(input_path, output_path, converter=converter) 39 | rtotype = to_bioimg(output_path, output_path_round, converter=converter) 40 | assert rfromtype == rtotype == OMEZarrConverter 41 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TileDB-Inc/TileDB-BioImaging/d505bf99c52fcc10f0f9faef50a17ab847250200/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_axes.py: -------------------------------------------------------------------------------- 1 | import itertools as it 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from tiledb.bioimg.converters.axes import Axes, Move, Squeeze, Swap, Unsqueeze 7 | 8 | 9 | class TestAxesMappers: 10 | @pytest.mark.parametrize( 11 | "s,i,j", [(b"ADCBE", 1, 3), (b"DBCAE", 3, 0), (b"ACBDE", 2, 1)] 12 | ) 13 | def test_swap(self, s, i, j): 14 | axes_mapper = Swap(i, j) 15 | b = bytearray(s) 16 | assert axes_mapper.transform_sequence(b) is None 17 | assert b == b"ABCDE" 18 | 19 | def test_swap_array(self): 20 | axes_mapper = Swap(1, 3) 21 | a = np.empty((5, 4, 8, 3, 6)) 22 | np.testing.assert_array_equal(axes_mapper.map_array(a), np.swapaxes(a, 1, 3)) 23 | 24 | @pytest.mark.parametrize( 25 | "s,i,j", 26 | [ 27 | (b"ADBCE", 1, 3), 28 | (b"ACDBE", 3, 1), 29 | (b"ACBDE", 1, 2), 30 | (b"ACBDE", 2, 1), 31 | (b"EABCD", 0, 4), 32 | (b"BCDEA", 4, 0), 33 | ], 34 | ) 35 | def test_move(self, s, i, j): 36 | axes_mapper = Move(i, j) 37 | b = bytearray(s) 38 | assert axes_mapper.transform_sequence(b) is None 39 | assert b == b"ABCDE" 40 | 41 | def test_move_array(self): 42 | axes_mapper = Move(1, 3) 43 | a = np.empty((5, 4, 8, 3, 6)) 44 | np.testing.assert_array_equal(axes_mapper.map_array(a), np.moveaxis(a, 1, 3)) 45 | 46 | @pytest.mark.parametrize( 47 | "s,idxs", 48 | [ 49 | (b"ADBC", (1,)), 50 | (b"ADBCF", (1, 4)), 51 | (b"ADBCEF", (1, 4, 5)), 52 | (b"DAEBFC", (0, 2, 4)), 53 | ], 54 | ) 55 | def test_squeeze(self, s, idxs): 56 | axes_mapper = Squeeze(idxs) 57 | b = bytearray(s) 58 | assert axes_mapper.transform_sequence(b) is None 59 | assert b == b"ABC" 60 | 61 | def test_squeeze_array(self): 62 | axes_mapper = Squeeze((1, 3)) 63 | a = np.empty((5, 1, 8, 1, 6)) 64 | np.testing.assert_array_equal(axes_mapper.map_array(a), np.squeeze(a, (1, 3))) 65 | 66 | @pytest.mark.parametrize( 67 | "s,idxs,t", 68 | [ 69 | (b"ABC", (1,), b"A_BC"), 70 | (b"ABC", (1, 2), b"A__BC"), 71 | (b"ABC", (1, 3), b"A_B_C"), 72 | (b"ABC", (1, 3, 4), b"A_B__C"), 73 | (b"ABC", (1, 3, 5), b"A_B_C_"), 74 | ], 75 | ) 76 | def test_unsqueeze(self, s, idxs, t): 77 | axes_mapper = Unsqueeze(idxs) 78 | b = bytearray(s) 79 | assert axes_mapper.transform_sequence(b, fill_value=ord("_")) is None 80 | assert b == t 81 | 82 | def test_unsqueeze_array(self): 83 | axes_mapper = Unsqueeze((1, 3)) 84 | a = np.empty((5, 8, 6)) 85 | np.testing.assert_array_equal( 86 | axes_mapper.map_array(a), np.expand_dims(a, (1, 3)) 87 | ) 88 | 89 | 90 | class TestAxes: 91 | def test_init(self): 92 | assert Axes("XYZ").dims == "XYZ" 93 | 94 | with pytest.raises(ValueError) as excinfo: 95 | Axes("XYZX") 96 | assert "Duplicate axes" in str(excinfo.value) 97 | 98 | with pytest.raises(ValueError) as excinfo: 99 | Axes("ZYTC") 100 | assert str(excinfo.value) == "Missing required axis 'X'" 101 | 102 | with pytest.raises(ValueError) as excinfo: 103 | Axes("XTC") 104 | assert str(excinfo.value) == "Missing required axis 'Y'" 105 | 106 | @pytest.mark.parametrize( 107 | "canonical_dims", 108 | ["YX", "ZYX", "CYX", "TYX", "CZYX", "TCYX", "TZYX", "TCZYX"], 109 | ) 110 | def test_canonical_unsqueezed(self, canonical_dims): 111 | shape = np.random.randint(2, 20, size=len(canonical_dims)) 112 | for axes in map(Axes, it.permutations(canonical_dims)): 113 | assert axes.canonical(shape).dims == canonical_dims 114 | 115 | def test_canonical_squeezed(self): 116 | shape = (1, 60, 40) 117 | for s in "ZXY", "CXY", "TXY": 118 | assert Axes(s).canonical(shape) == Axes("YX") 119 | 120 | shape = (1, 1, 60, 40) 121 | for s in "CZXY", "TCXY", "TZXY": 122 | assert Axes(s).canonical(shape) == Axes("YX") 123 | 124 | shape = (3, 1, 60, 40) 125 | assert Axes("CZXY").canonical(shape) == Axes("CYX") 126 | assert Axes("TCXY").canonical(shape) == Axes("TYX") 127 | assert Axes("ZTXY").canonical(shape) == Axes("ZYX") 128 | 129 | shape = (1, 1, 1, 60, 40) 130 | for s in "TCZXY", "TZCXY", "CZTXY": 131 | assert Axes(s).canonical(shape) == Axes("YX") 132 | 133 | shape = (1, 3, 1, 60, 40) 134 | assert Axes("TCZXY").canonical(shape) == Axes("CYX") 135 | assert Axes("ZTCXY").canonical(shape) == Axes("TYX") 136 | assert Axes("CZTXY").canonical(shape) == Axes("ZYX") 137 | 138 | shape = (7, 3, 1, 60, 40) 139 | assert Axes("CTZXY").canonical(shape) == Axes("TCYX") 140 | assert Axes("ZTCXY").canonical(shape) == Axes("TZYX") 141 | assert Axes("ZCTXY").canonical(shape) == Axes("CZYX") 142 | 143 | 144 | class TestCompositeAxesMapper: 145 | def test_canonical_transform_2d(self): 146 | a = np.random.rand(60, 40) 147 | assert_canonical_transform("YX", a, a) 148 | assert_canonical_transform("XY", a, np.swapaxes(a, 0, 1)) 149 | 150 | def test_canonical_transform_3d(self): 151 | a = np.random.rand(10, 60, 40) 152 | for s in "ZYX", "CYX", "TYX": 153 | assert_canonical_transform(s, a, a) 154 | for s in "XYZ", "XYC", "XYT": 155 | assert_canonical_transform(s, a, np.swapaxes(a, 0, 2)) 156 | for s in "ZXY", "CXY", "TXY": 157 | assert_canonical_transform(s, a, np.swapaxes(a, 1, 2)) 158 | for s in "YXZ", "YXC", "YXT": 159 | assert_canonical_transform(s, a, np.moveaxis(a, 2, 0)) 160 | 161 | def test_canonical_transform_4d(self): 162 | a = np.random.rand(3, 10, 60, 40) 163 | for s in "CZYX", "TCYX", "TZYX": 164 | assert_canonical_transform(s, a, a) 165 | for s in "ZCYX", "CTYX", "ZTYX": 166 | assert_canonical_transform(s, a, np.swapaxes(a, 0, 1)) 167 | for s in "CZXY", "TCXY", "TZXY": 168 | assert_canonical_transform(s, a, np.swapaxes(a, 2, 3)) 169 | for s in "ZYXC", "CYXT", "ZYXT": 170 | assert_canonical_transform(s, a, np.moveaxis(a, 3, 0)) 171 | for s in "CYXZ", "TYXC", "TYXZ": 172 | assert_canonical_transform(s, a, np.moveaxis(a, 3, 1)) 173 | for s in "YXZC", "YXCT", "YXZT": 174 | assert_canonical_transform(s, a, np.moveaxis(np.moveaxis(a, 2, 0), 3, 0)) 175 | for s in "YXCZ", "YXTC", "YXTZ": 176 | assert_canonical_transform(s, a, np.moveaxis(np.moveaxis(a, 2, 0), 3, 1)) 177 | for s in "ZCXY", "CTXY", "ZTXY": 178 | assert_canonical_transform(s, a, np.swapaxes(np.swapaxes(a, 0, 1), 2, 3)) 179 | for s in "XYZC", "XYCT", "XYZT": 180 | assert_canonical_transform(s, a, np.swapaxes(np.swapaxes(a, 0, 3), 1, 2)) 181 | for s in "CXYZ", "TXYC", "TXYZ": 182 | assert_canonical_transform(s, a, np.swapaxes(np.moveaxis(a, 3, 1), 2, 3)) 183 | for s in "ZXYC", "CXYT", "ZXYT": 184 | assert_canonical_transform(s, a, np.swapaxes(np.moveaxis(a, 3, 0), 2, 3)) 185 | for s in "XYCZ", "XYTC", "XYTZ": 186 | assert_canonical_transform(s, a, np.swapaxes(np.moveaxis(a, 2, 0), 1, 3)) 187 | 188 | def test_canonical_transform_5d(self): 189 | a = np.random.rand(7, 3, 10, 60, 40) 190 | assert_canonical_transform("TCZYX", a, a) 191 | 192 | assert_canonical_transform("CTZYX", a, np.swapaxes(a, 0, 1)) 193 | assert_canonical_transform("ZCTYX", a, np.swapaxes(a, 0, 2)) 194 | assert_canonical_transform("TZCYX", a, np.swapaxes(a, 1, 2)) 195 | assert_canonical_transform("TCXYZ", a, np.swapaxes(a, 2, 4)) 196 | assert_canonical_transform("TCZXY", a, np.swapaxes(a, 3, 4)) 197 | 198 | assert_canonical_transform("ZTCYX", a, np.moveaxis(a, 0, 2)) 199 | assert_canonical_transform("CZTYX", a, np.moveaxis(a, 2, 0)) 200 | assert_canonical_transform("CZYXT", a, np.moveaxis(a, 4, 0)) 201 | assert_canonical_transform("TZYXC", a, np.moveaxis(a, 4, 1)) 202 | assert_canonical_transform("TCYXZ", a, np.moveaxis(a, 4, 2)) 203 | 204 | assert_canonical_transform("CTXYZ", a, np.swapaxes(np.swapaxes(a, 0, 1), 2, 4)) 205 | assert_canonical_transform("CTZXY", a, np.swapaxes(np.swapaxes(a, 0, 1), 3, 4)) 206 | assert_canonical_transform("ZCTXY", a, np.swapaxes(np.swapaxes(a, 0, 2), 3, 4)) 207 | assert_canonical_transform("YXZTC", a, np.swapaxes(np.swapaxes(a, 0, 3), 1, 4)) 208 | assert_canonical_transform("XYZCT", a, np.swapaxes(np.swapaxes(a, 0, 4), 1, 3)) 209 | assert_canonical_transform("ZCXYT", a, np.swapaxes(np.swapaxes(a, 0, 4), 2, 4)) 210 | assert_canonical_transform("TZCXY", a, np.swapaxes(np.swapaxes(a, 1, 2), 3, 4)) 211 | assert_canonical_transform("TZXYC", a, np.swapaxes(np.swapaxes(a, 1, 4), 2, 4)) 212 | 213 | assert_canonical_transform("ZTXYC", a, np.swapaxes(np.moveaxis(a, 0, 2), 1, 4)) 214 | assert_canonical_transform("ZTCXY", a, np.swapaxes(np.moveaxis(a, 0, 2), 3, 4)) 215 | assert_canonical_transform("YXCZT", a, np.swapaxes(np.moveaxis(a, 0, 3), 0, 4)) 216 | assert_canonical_transform("XYCZT", a, np.swapaxes(np.moveaxis(a, 1, 3), 0, 4)) 217 | assert_canonical_transform("CZTXY", a, np.swapaxes(np.moveaxis(a, 2, 0), 3, 4)) 218 | assert_canonical_transform("CXYTZ", a, np.swapaxes(np.moveaxis(a, 3, 0), 2, 4)) 219 | assert_canonical_transform("CXYZT", a, np.swapaxes(np.moveaxis(a, 4, 0), 2, 4)) 220 | assert_canonical_transform("CZXYT", a, np.swapaxes(np.moveaxis(a, 4, 0), 3, 4)) 221 | assert_canonical_transform("ZTYXC", a, np.swapaxes(np.moveaxis(a, 4, 1), 0, 2)) 222 | assert_canonical_transform("TXYZC", a, np.swapaxes(np.moveaxis(a, 4, 1), 2, 4)) 223 | assert_canonical_transform("CTYXZ", a, np.swapaxes(np.moveaxis(a, 4, 2), 0, 1)) 224 | assert_canonical_transform("TXYCZ", a, np.swapaxes(np.moveaxis(a, 4, 2), 1, 4)) 225 | 226 | assert_canonical_transform("XYTCZ", a, np.moveaxis(np.moveaxis(a, 0, 4), 0, 3)) 227 | assert_canonical_transform("YXTCZ", a, np.moveaxis(np.moveaxis(a, 0, 4), 0, 4)) 228 | assert_canonical_transform("TYXCZ", a, np.moveaxis(np.moveaxis(a, 1, 4), 1, 4)) 229 | assert_canonical_transform("ZYXTC", a, np.moveaxis(np.moveaxis(a, 3, 0), 4, 1)) 230 | assert_canonical_transform("CYXTZ", a, np.moveaxis(np.moveaxis(a, 3, 0), 4, 2)) 231 | assert_canonical_transform("TYXZC", a, np.moveaxis(np.moveaxis(a, 4, 1), 4, 2)) 232 | assert_canonical_transform("ZCYXT", a, np.moveaxis(np.moveaxis(a, 4, 0), 1, 2)) 233 | assert_canonical_transform("ZYXCT", a, np.moveaxis(np.moveaxis(a, 4, 0), 4, 1)) 234 | assert_canonical_transform("CYXZT", a, np.moveaxis(np.moveaxis(a, 4, 0), 4, 2)) 235 | 236 | def test_transform_squeeze(self): 237 | a = np.random.rand(1, 60, 40) 238 | assert_transform("ZXY", "XY", a, np.squeeze(a, 0)) 239 | assert_transform("ZXY", "YX", a, np.swapaxes(np.squeeze(a, 0), 0, 1)) 240 | 241 | a = np.random.rand(1, 1, 60, 40) 242 | assert_transform("CZXY", "XY", a, np.squeeze(a, (0, 1))) 243 | assert_transform("CZXY", "CXY", a, np.squeeze(a, 1)) 244 | assert_transform("CZXY", "ZXY", a, np.squeeze(a, 0)) 245 | assert_transform("CZXY", "YX", a, np.swapaxes(np.squeeze(a, (0, 1)), 0, 1)) 246 | assert_transform("CZXY", "CYX", a, np.swapaxes(np.squeeze(a, 1), 1, 2)) 247 | assert_transform("CZXY", "ZYX", a, np.swapaxes(np.squeeze(a, 0), 1, 2)) 248 | 249 | a = np.random.rand(1, 1, 1, 60, 40) 250 | assert_transform("TCZXY", "XY", a, np.squeeze(a, (0, 1, 2))) 251 | assert_transform("TCZXY", "ZXY", a, np.squeeze(a, (0, 1))) 252 | assert_transform("TCZXY", "CXY", a, np.squeeze(a, (0, 2))) 253 | assert_transform("TCZXY", "TXY", a, np.squeeze(a, (1, 2))) 254 | assert_transform("TCZXY", "CZXY", a, np.squeeze(a, 0)) 255 | assert_transform("TCZXY", "TZXY", a, np.squeeze(a, 1)) 256 | assert_transform("TCZXY", "TCXY", a, np.squeeze(a, 2)) 257 | assert_transform("TCZXY", "YX", a, np.swapaxes(np.squeeze(a, (0, 1, 2)), 0, 1)) 258 | assert_transform("TCZXY", "ZYX", a, np.swapaxes(np.squeeze(a, (0, 1)), 1, 2)) 259 | assert_transform("TCZXY", "CYX", a, np.swapaxes(np.squeeze(a, (0, 2)), 1, 2)) 260 | assert_transform("TCZXY", "TYX", a, np.swapaxes(np.squeeze(a, (1, 2)), 1, 2)) 261 | assert_transform("TCZXY", "CZYX", a, np.swapaxes(np.squeeze(a, 0), 2, 3)) 262 | assert_transform("TCZXY", "TZYX", a, np.swapaxes(np.squeeze(a, 1), 2, 3)) 263 | assert_transform("TCZXY", "TCYX", a, np.swapaxes(np.squeeze(a, 2), 2, 3)) 264 | 265 | def test_transform_expand(self): 266 | a = np.random.rand(10, 60, 40) 267 | assert_transform("CYX", "ZCYX", a, np.expand_dims(a, 0)) 268 | assert_transform("CYX", "CZYX", a, np.expand_dims(a, 1)) 269 | assert_transform("CYX", "TZCYX", a, np.expand_dims(a, (0, 1))) 270 | assert_transform("CYX", "TCZYX", a, np.expand_dims(a, (0, 2))) 271 | assert_transform("CYX", "CTZYX", a, np.expand_dims(a, (1, 2))) 272 | assert_transform("CYX", "ZCXY", a, np.swapaxes(np.expand_dims(a, 0), 2, 3)) 273 | assert_transform("CYX", "CZXY", a, np.swapaxes(np.expand_dims(a, 1), 2, 3)) 274 | assert_transform( 275 | "CYX", "TZCXY", a, np.swapaxes(np.expand_dims(a, (0, 1)), 3, 4) 276 | ) 277 | assert_transform( 278 | "CYX", "TCZXY", a, np.swapaxes(np.expand_dims(a, (0, 2)), 3, 4) 279 | ) 280 | assert_transform( 281 | "CYX", "CTZXY", a, np.swapaxes(np.expand_dims(a, (1, 2)), 3, 4) 282 | ) 283 | 284 | 285 | def assert_transform(source, target, a, expected): 286 | axes_mapper = Axes(source).mapper(Axes(target)) 287 | assert axes_mapper.map_shape(a.shape) == expected.shape 288 | np.testing.assert_array_equal(axes_mapper.map_array(a), expected) 289 | 290 | 291 | def assert_canonical_transform(source, a, expected): 292 | source = Axes(source) 293 | target = source.canonical(a.shape) 294 | axes_mapper = source.mapper(target) 295 | assert axes_mapper.map_shape(a.shape) == expected.shape 296 | np.testing.assert_array_equal(axes_mapper.map_array(a), expected) 297 | -------------------------------------------------------------------------------- /tests/unit/test_helpers.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | import tiledb 7 | from tiledb.bioimg.helpers import ( 8 | get_decimal_from_rgba, 9 | get_pixel_depth, 10 | get_rgba, 11 | iter_color, 12 | merge_ned_ranges, 13 | remove_ome_image_metadata, 14 | ) 15 | 16 | from .. import generate_test_case, generate_xml 17 | 18 | 19 | def test_color_iterator(): 20 | generator_grayscale = iter_color(np.dtype(np.uint8), 1) 21 | generator_rgb = iter_color(np.dtype(np.uint8), 3) 22 | generator_random = iter_color(np.dtype(np.uint8), 5) 23 | 24 | for _, color in zip(range(1), generator_grayscale): 25 | assert color == get_rgba(get_decimal_from_rgba(color)) 26 | assert color == {"red": 255, "green": 255, "blue": 255, "alpha": 255} 27 | 28 | for idx, color in zip(range(3), generator_rgb): 29 | assert color == get_rgba(get_decimal_from_rgba(color)) 30 | assert color == { 31 | "red": 255 if idx == 0 else 0, 32 | "green": 255 if idx == 1 else 0, 33 | "blue": 255 if idx == 2 else 0, 34 | "alpha": 255, 35 | } 36 | 37 | for _, color in zip(range(5), generator_random): 38 | assert color == get_rgba(get_decimal_from_rgba(color)) 39 | 40 | with pytest.raises(NotImplementedError): 41 | generator_non_float = iter_color(np.dtype(float), 5) 42 | for _, color in zip(range(5), generator_non_float): 43 | pass 44 | 45 | 46 | def test_get_pixel_depth(): 47 | webp_filter = tiledb.WebpFilter() 48 | # Test that for some reason input_format gets a random value not supported 49 | webp_filter._input_format = 6 50 | with pytest.raises(ValueError) as err: 51 | get_pixel_depth(webp_filter) 52 | assert "Invalid WebpInputFormat" in str(err) 53 | 54 | 55 | @pytest.mark.parametrize( 56 | "num_axes, num_ranges, max_value", 57 | [(5, 10, 100), (3, 20, 50), (4, 15, 200), (6, 25, 300)], 58 | ) 59 | def test_validate_ingestion(num_axes, num_ranges, max_value): 60 | input_ranges, expected_output = generate_test_case(num_axes, num_ranges, max_value) 61 | assert merge_ned_ranges(input_ranges) == expected_output 62 | 63 | 64 | @pytest.mark.parametrize("macro", [True, False]) 65 | @pytest.mark.parametrize("has_label", [True, False]) 66 | @pytest.mark.parametrize("annotations", [True, False]) 67 | @pytest.mark.parametrize("num_images", [1, 2, 3]) 68 | @pytest.mark.parametrize("root_tag", ["OME", "InvalidRoot"]) 69 | def test_remove_ome_image_metadata(macro, has_label, annotations, num_images, root_tag): 70 | original_xml_string = generate_xml( 71 | has_macro=macro, 72 | has_label=has_label, 73 | has_annotations=annotations, 74 | num_images=1, 75 | root_tag=root_tag, 76 | ) 77 | 78 | excluded_metadata = remove_ome_image_metadata(original_xml_string) 79 | 80 | namespaces = {"ome": "http://www.openmicroscopy.org/Schemas/OME/2016-06"} 81 | 82 | barcode_xpath = ".//ome:StructuredAnnotations/ome:CommentAnnotation[ome:Description='barcode_value']" 83 | label_xpath = ".//ome:StructuredAnnotations/ome:CommentAnnotation[ome:Description='label_text']" 84 | 85 | if root_tag == "OME": 86 | parsed_excluded = ET.fromstring(excluded_metadata) 87 | 88 | # Assert if "label" subelement is present 89 | assert ( 90 | parsed_excluded.find( 91 | ".//{http://www.openmicroscopy.org/Schemas/OME/2016-06}Image[@ID='Image:label'][@Name='label']" 92 | ) 93 | is None 94 | ) 95 | 96 | # Assert if "macro" subelement is present 97 | assert ( 98 | parsed_excluded.find( 99 | ".//{http://www.openmicroscopy.org/Schemas/OME/2016-06}Image[@ID='Image:macro'][@Name='macro']" 100 | ) 101 | is None 102 | ) 103 | 104 | # Assert if "barcode_value" and "label_text" subelement is present 105 | assert parsed_excluded.find(barcode_xpath, namespaces=namespaces) is None 106 | assert parsed_excluded.find(label_xpath, namespaces=namespaces) is None 107 | 108 | else: 109 | assert remove_ome_image_metadata(original_xml_string) is None 110 | -------------------------------------------------------------------------------- /tests/unit/test_openslide.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import pytest 4 | 5 | import tiledb 6 | from tests import get_schema 7 | from tiledb.bioimg.helpers import open_bioimg 8 | from tiledb.bioimg.openslide import TileDBOpenSlide 9 | 10 | from .. import ATTR_NAME 11 | 12 | 13 | class TestTileDBOpenSlide: 14 | def test(self, tmp_path): 15 | def r(): 16 | return random.randint(64, 4096) 17 | 18 | level_dimensions = [(r(), r()) for _ in range(random.randint(1, 10))] 19 | schemas = [get_schema(*dims) for dims in level_dimensions] 20 | group_path = str(tmp_path) 21 | tiledb.Group.create(group_path) 22 | with tiledb.Group(group_path, "w") as G: 23 | for level, schema in enumerate(schemas): 24 | level_path = str(tmp_path / f"l_{level}.tdb") 25 | tiledb.Array.create(level_path, schema) 26 | with open_bioimg(level_path, "w") as A: 27 | A.meta["level"] = level 28 | G.add(level_path) 29 | 30 | with TileDBOpenSlide(group_path) as t: 31 | assert t.level_count == len(level_dimensions) 32 | assert t.level_dimensions == tuple(level_dimensions) 33 | 34 | with TileDBOpenSlide(group_path, attr=ATTR_NAME) as t: 35 | assert t.level_count == len(level_dimensions) 36 | assert t.level_dimensions == tuple(level_dimensions) 37 | 38 | with pytest.raises(KeyError) as e_info: 39 | _ = TileDBOpenSlide(group_path, attr="test_attr_name") 40 | assert "No attribute matching 'test_attr_name'" in str(e_info.value) 41 | -------------------------------------------------------------------------------- /tests/unit/test_tiles.py: -------------------------------------------------------------------------------- 1 | import tiledb 2 | from tiledb.bioimg.converters.tiles import dim_range, iter_slices, iter_tiles 3 | 4 | domain = tiledb.Domain( 5 | tiledb.Dim("X", domain=(0, 9), tile=3), 6 | tiledb.Dim("Y", domain=(0, 14), tile=5), 7 | tiledb.Dim("Z", domain=(0, 6), tile=4), 8 | tiledb.Dim("C", domain=(0, 2), tile=3), 9 | ) 10 | 11 | 12 | def test_dim_range(): 13 | dims = list(domain) 14 | assert dim_range( 15 | (int(dims[0].domain[0]), int(dims[0].domain[1]), int(dims[0].tile)) 16 | ) == range(0, 10, 3) 17 | assert dim_range( 18 | (int(dims[1].domain[0]), int(dims[1].domain[1]), int(dims[1].tile)) 19 | ) == range(0, 15, 5) 20 | assert dim_range( 21 | (int(dims[2].domain[0]), int(dims[2].domain[1]), int(dims[2].tile)) 22 | ) == range(0, 7, 4) 23 | assert dim_range( 24 | (int(dims[3].domain[0]), int(dims[3].domain[1]), int(dims[3].tile)) 25 | ) == range(0, 3, 3) 26 | 27 | 28 | def test_iter_slices(): 29 | assert list(iter_slices(range(0, 9, 3))) == [slice(0, 3), slice(3, 6), slice(6, 9)] 30 | assert list(iter_slices(range(0, 10, 3))) == [ 31 | slice(0, 3), 32 | slice(3, 6), 33 | slice(6, 9), 34 | slice(9, 10), 35 | ] 36 | 37 | 38 | def test_iter_tiles(): 39 | assert list(iter_tiles(domain)) == [ 40 | (slice(0, 3), slice(0, 5), slice(0, 4), slice(0, 3)), 41 | (slice(0, 3), slice(0, 5), slice(4, 7), slice(0, 3)), 42 | (slice(0, 3), slice(5, 10), slice(0, 4), slice(0, 3)), 43 | (slice(0, 3), slice(5, 10), slice(4, 7), slice(0, 3)), 44 | (slice(0, 3), slice(10, 15), slice(0, 4), slice(0, 3)), 45 | (slice(0, 3), slice(10, 15), slice(4, 7), slice(0, 3)), 46 | (slice(3, 6), slice(0, 5), slice(0, 4), slice(0, 3)), 47 | (slice(3, 6), slice(0, 5), slice(4, 7), slice(0, 3)), 48 | (slice(3, 6), slice(5, 10), slice(0, 4), slice(0, 3)), 49 | (slice(3, 6), slice(5, 10), slice(4, 7), slice(0, 3)), 50 | (slice(3, 6), slice(10, 15), slice(0, 4), slice(0, 3)), 51 | (slice(3, 6), slice(10, 15), slice(4, 7), slice(0, 3)), 52 | (slice(6, 9), slice(0, 5), slice(0, 4), slice(0, 3)), 53 | (slice(6, 9), slice(0, 5), slice(4, 7), slice(0, 3)), 54 | (slice(6, 9), slice(5, 10), slice(0, 4), slice(0, 3)), 55 | (slice(6, 9), slice(5, 10), slice(4, 7), slice(0, 3)), 56 | (slice(6, 9), slice(10, 15), slice(0, 4), slice(0, 3)), 57 | (slice(6, 9), slice(10, 15), slice(4, 7), slice(0, 3)), 58 | (slice(9, 10), slice(0, 5), slice(0, 4), slice(0, 3)), 59 | (slice(9, 10), slice(0, 5), slice(4, 7), slice(0, 3)), 60 | (slice(9, 10), slice(5, 10), slice(0, 4), slice(0, 3)), 61 | (slice(9, 10), slice(5, 10), slice(4, 7), slice(0, 3)), 62 | (slice(9, 10), slice(10, 15), slice(0, 4), slice(0, 3)), 63 | (slice(9, 10), slice(10, 15), slice(4, 7), slice(0, 3)), 64 | ] 65 | -------------------------------------------------------------------------------- /tiledb/bioimg/__init__.py: -------------------------------------------------------------------------------- 1 | ATTR_NAME = "intensity" 2 | WHITE_RGB = 16777215 # FFFFFF in hex 3 | WHITE_RGBA = 4294967295 # FFFFFFFF in hex 4 | EXPORT_TILE_SIZE = 256 5 | 6 | import importlib.util 7 | import warnings 8 | from typing import Optional 9 | from .types import Converters 10 | 11 | _osd_exc: Optional[ImportError] 12 | try: 13 | importlib.util.find_spec("openslide") 14 | except ImportError as err_osd: 15 | warnings.warn( 16 | "Openslide Converter requires 'openslide-python' package. " 17 | "You can install 'tiledb-bioimg' with the 'openslide' or 'full' flag" 18 | ) 19 | _osd_exc = err_osd 20 | else: 21 | _osd_exc = None 22 | 23 | from .wrappers import * 24 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/__init__.py: -------------------------------------------------------------------------------- 1 | FMT_VERSION = 2 2 | DATASET_TYPE = "bioimg" 3 | DEFAULT_SCRATCH_SPACE = "/dev/shm" 4 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/axes.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | from typing import Any, Iterable, Iterator, MutableSequence, Sequence, Set, Tuple 6 | 7 | import numpy as np 8 | from pyeditdistance.distance import levenshtein 9 | 10 | 11 | class AxesMapper(ABC): 12 | @property 13 | @abstractmethod 14 | def inverse(self) -> AxesMapper: 15 | """The axes mapper that inverts the effect of this one""" 16 | 17 | @abstractmethod 18 | def map_array(self, a: np.ndarray) -> np.ndarray: 19 | """Return the transformed Numpy array""" 20 | 21 | def map_shape(self, shape: Tuple[int, ...]) -> Tuple[int, ...]: 22 | """Return the shape of the transformed Numpy array.""" 23 | mapped_shape = list(shape) 24 | self.transform_shape(mapped_shape) 25 | return tuple(mapped_shape) 26 | 27 | def map_tile(self, tile: Tuple[slice, ...]) -> Tuple[slice, ...]: 28 | """Return the tile for slicing the transformed Numpy array""" 29 | mapped_tile = list(tile) 30 | self.transform_tile(mapped_tile) 31 | return tuple(mapped_tile) 32 | 33 | def transform_shape(self, shape: MutableSequence[int]) -> None: 34 | """Transform the given shape in place""" 35 | self.transform_sequence(shape) 36 | 37 | def transform_tile(self, tile: MutableSequence[slice]) -> None: 38 | """Transform the given tile in place""" 39 | self.transform_sequence(tile) 40 | 41 | def transform_sequence(self, s: MutableSequence[Any]) -> None: 42 | """Transform the given mutable sequence in place""" 43 | # intentionally not decorated as @abstractmethod: subclasses may override 44 | # transform_shape and transform_tile instead 45 | raise NotImplementedError 46 | 47 | 48 | @dataclass(frozen=True) 49 | class Swap(AxesMapper): 50 | i: int 51 | j: int 52 | 53 | @property 54 | def inverse(self) -> AxesMapper: 55 | return self 56 | 57 | def map_array(self, a: np.ndarray) -> np.ndarray: 58 | return np.swapaxes(a, self.i, self.j) 59 | 60 | def transform_sequence(self, s: MutableSequence[Any]) -> None: 61 | i, j = self.i, self.j 62 | s[i], s[j] = s[j], s[i] 63 | 64 | 65 | @dataclass(frozen=True) 66 | class Move(AxesMapper): 67 | i: int 68 | j: int 69 | 70 | @property 71 | def inverse(self) -> AxesMapper: 72 | return Move(self.j, self.i) 73 | 74 | def map_array(self, a: np.ndarray) -> np.ndarray: 75 | return np.moveaxis(a, self.i, self.j) 76 | 77 | def transform_sequence(self, s: MutableSequence[Any]) -> None: 78 | s.insert(self.j, s.pop(self.i)) 79 | 80 | 81 | @dataclass(frozen=True) 82 | class Squeeze(AxesMapper): 83 | idxs: Tuple[int, ...] 84 | 85 | @property 86 | def inverse(self) -> AxesMapper: 87 | return Unsqueeze(self.idxs) 88 | 89 | def map_array(self, a: np.ndarray) -> np.ndarray: 90 | return np.squeeze(a, self.idxs) 91 | 92 | def transform_sequence(self, s: MutableSequence[Any]) -> None: 93 | for i in sorted(self.idxs, reverse=True): 94 | del s[i] 95 | 96 | 97 | @dataclass(frozen=True) 98 | class Unsqueeze(AxesMapper): 99 | idxs: Tuple[int, ...] 100 | 101 | @property 102 | def inverse(self) -> AxesMapper: 103 | return Squeeze(self.idxs) 104 | 105 | def map_array(self, a: np.ndarray) -> np.ndarray: 106 | return np.expand_dims(a, self.idxs) 107 | 108 | def transform_shape(self, shape: MutableSequence[int]) -> None: 109 | self.transform_sequence(shape, fill_value=1) 110 | 111 | def transform_tile(self, tile: MutableSequence[slice]) -> None: 112 | self.transform_sequence(tile, fill_value=slice(None)) 113 | 114 | def transform_sequence( 115 | self, sequence: MutableSequence[Any], fill_value: Any = None 116 | ) -> None: 117 | for i in sorted(self.idxs): 118 | sequence.insert(i, fill_value) 119 | 120 | 121 | @dataclass(frozen=True) 122 | class YXC_TO_YX(AxesMapper): 123 | c_size: int 124 | 125 | @property 126 | def inverse(self) -> AxesMapper: 127 | return YX_TO_YXC(self.c_size) 128 | 129 | def map_array(self, a: np.ndarray) -> np.ndarray: 130 | return a.reshape(self.map_shape(a.shape)) 131 | 132 | def transform_shape(self, shape: MutableSequence[int]) -> None: 133 | y, x, c = shape 134 | if c != self.c_size: 135 | raise ValueError(f"C dimension must have size {self.c_size}: {c} given") 136 | shape[1] *= c 137 | del shape[2] 138 | 139 | def transform_tile(self, tile: MutableSequence[slice]) -> None: 140 | y, x, c = tile 141 | if c != slice(0, self.c_size): 142 | raise ValueError( 143 | f"C dimension can cannot be sliced: {c} given - {slice(0, self.c_size)} expected" 144 | ) 145 | tile[1] = slice(x.start * self.c_size, x.stop * self.c_size) 146 | del tile[2] 147 | 148 | 149 | @dataclass(frozen=True) 150 | class YX_TO_YXC(AxesMapper): 151 | c_size: int 152 | 153 | @property 154 | def inverse(self) -> AxesMapper: 155 | return YXC_TO_YX(self.c_size) 156 | 157 | def map_array(self, a: np.ndarray) -> np.ndarray: 158 | return a.reshape(self.map_shape(a.shape)) 159 | 160 | def transform_shape(self, shape: MutableSequence[int]) -> None: 161 | c = self.c_size 162 | shape[1] //= c 163 | shape.append(c) 164 | 165 | def transform_tile(self, tile: MutableSequence[slice]) -> None: 166 | c = self.c_size 167 | tile[1] = slice(tile[1].start // c, tile[1].stop // c) 168 | tile.append(slice(0, c)) 169 | 170 | 171 | @dataclass(frozen=True) 172 | class CompositeAxesMapper(AxesMapper): 173 | mappers: Sequence[AxesMapper] 174 | 175 | @property 176 | def inverse(self) -> AxesMapper: 177 | return CompositeAxesMapper([t.inverse for t in reversed(self.mappers)]) 178 | 179 | def map_array(self, a: np.ndarray) -> np.ndarray: 180 | for mapper in self.mappers: 181 | a = mapper.map_array(a) 182 | return a 183 | 184 | def transform_shape(self, shape: MutableSequence[int]) -> None: 185 | for mapper in self.mappers: 186 | mapper.transform_shape(shape) 187 | 188 | def transform_tile(self, tile: MutableSequence[slice]) -> None: 189 | for mapper in self.mappers: 190 | mapper.transform_tile(tile) 191 | 192 | def transform_sequence(self, s: MutableSequence[Any]) -> None: 193 | for mapper in self.mappers: 194 | mapper.transform_sequence(s) 195 | 196 | 197 | @dataclass(frozen=True) 198 | class Axes: 199 | dims: str 200 | __slots__ = ("dims",) 201 | CANONICAL_DIMS = "TCZYX" 202 | 203 | def __init__(self, dims: Iterable[str]): 204 | if not isinstance(dims, str): 205 | dims = "".join(dims) 206 | axes = set(dims) 207 | if len(dims) != len(axes): 208 | raise ValueError(f"Duplicate axes: {dims}") 209 | for required_axis in "X", "Y": 210 | if required_axis not in axes: 211 | raise ValueError(f"Missing required axis {required_axis!r}") 212 | 213 | axes.difference_update(self.CANONICAL_DIMS) 214 | if axes: 215 | if len(axes) == 1: 216 | # Assign extra/custom dimension as Time TBC 217 | dims = self._canonical_transformation(dims, axes) 218 | else: 219 | raise ValueError(f"{axes.pop()!r} is not a valid Axis") 220 | object.__setattr__(self, "dims", dims) 221 | 222 | @staticmethod 223 | def _canonical_transformation(dims: str, axes: Set[str]) -> str: 224 | custom_axis = f"{axes.pop()}".replace("'", "") 225 | if "T" not in dims: 226 | dims.replace(custom_axis, "T") 227 | elif "Z" not in dims: 228 | dims.replace(custom_axis, "Z") 229 | else: 230 | raise ValueError(f"{custom_axis!r} cannot be mapped to a canonical value") 231 | return dims 232 | 233 | def canonical(self, shape: Tuple[int, ...]) -> Axes: 234 | """ 235 | Return a new Axes instance with the dimensions of this axes whose size in `shape` 236 | are greater than 1 and ordered in canonical order (TCZYX) 237 | """ 238 | assert len(self.dims) == len(shape) 239 | dims = frozenset(dim for dim, size in zip(self.dims, shape) if size > 1) 240 | return Axes(dim for dim in self.CANONICAL_DIMS if dim in dims) 241 | 242 | def mapper(self, other: Axes) -> AxesMapper: 243 | """Return an AxesMapper from this axes to other""" 244 | return CompositeAxesMapper(list(_iter_axes_mappers(self.dims, other.dims))) 245 | 246 | def webp_mapper(self, num_channels: int) -> AxesMapper: 247 | """Return an AxesMapper from this 3D axes (YXC or a permutation) to 2D (YX)""" 248 | mappers = list(_iter_axes_mappers(self.dims, "YXC")) 249 | mappers.append(YXC_TO_YX(num_channels)) 250 | return CompositeAxesMapper(mappers) 251 | 252 | 253 | def _iter_axes_mappers(s: str, t: str) -> Iterator[AxesMapper]: 254 | s_set = frozenset(s) 255 | assert len(s_set) == len(s), f"{s!r} contains duplicates" 256 | t_set = frozenset(t) 257 | assert len(t_set) == len(t), f"{t!r} contains duplicates" 258 | 259 | common, squeeze_axes = [], [] 260 | for i, m in enumerate(s): 261 | if m in t_set: 262 | common.append(m) 263 | else: 264 | squeeze_axes.append(i) 265 | if squeeze_axes: 266 | # source has extra dims: squeeze them 267 | yield Squeeze(tuple(squeeze_axes)) 268 | s = "".join(common) 269 | s_set = frozenset(s) 270 | 271 | missing = t_set - s_set 272 | if missing: 273 | # source has missing dims: expand them 274 | yield Unsqueeze(tuple(range(len(missing)))) 275 | s = "".join(missing) + s 276 | s_set = frozenset(s) 277 | 278 | # source has the same dims: transpose them 279 | assert s_set == t_set 280 | n = len(s) 281 | sbuf = bytearray(s.encode()) 282 | tbuf = t.encode() 283 | while sbuf != tbuf: 284 | min_distance = np.inf 285 | for candidate_transpose in _iter_transpositions(n): 286 | buf = bytearray(sbuf) 287 | candidate_transpose.transform_sequence(buf) 288 | distance = levenshtein(buf.decode(), t) 289 | if distance < min_distance: 290 | best_transpose = candidate_transpose 291 | min_distance = distance 292 | yield best_transpose 293 | best_transpose.transform_sequence(sbuf) 294 | 295 | 296 | def _iter_transpositions(n: int) -> Iterator[AxesMapper]: 297 | for i in range(n): 298 | for j in range(i + 1, n): 299 | yield Swap(i, j) 300 | yield Move(i, j) 301 | yield Move(j, i) 302 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/io.py: -------------------------------------------------------------------------------- 1 | import math 2 | import warnings 3 | from logging import Logger 4 | from typing import ( 5 | Any, 6 | Dict, 7 | Iterator, 8 | MutableSequence, 9 | Optional, 10 | Sequence, 11 | Tuple, 12 | Union, 13 | ) 14 | 15 | import numpy 16 | from numpy._typing import NDArray 17 | from tifffile import TiffPage 18 | from tifffile.tifffile import ( 19 | COMPRESSION, 20 | TiffFrame, 21 | ) 22 | 23 | # TODO: Add support for 3D images 24 | 25 | 26 | def as_array( 27 | page: Union[TiffPage, TiffFrame], logger: Logger, buffer_size: Optional[int] = None 28 | ) -> Iterator[Tuple[NDArray[Any], Tuple[int, ...]]]: 29 | keyframe = page.keyframe # self or keyframe 30 | fh = page.parent.filehandle 31 | lock = fh.lock 32 | with lock: 33 | closed = fh.closed 34 | if closed: 35 | warnings.warn(f"{page!r} reading array from closed file", UserWarning) 36 | fh.open() 37 | keyframe.decode # init TiffPage.decode function under lock 38 | 39 | decodeargs: Dict[str, Any] = {"_fullsize": bool(False)} 40 | if keyframe.compression in { 41 | COMPRESSION.OJPEG, 42 | COMPRESSION.JPEG, 43 | COMPRESSION.JPEG_LOSSY, 44 | COMPRESSION.ALT_JPEG, 45 | }: # JPEG 46 | decodeargs["jpegtables"] = page.jpegtables 47 | decodeargs["jpegheader"] = keyframe.jpegheader 48 | 49 | segment_cache: MutableSequence[Optional[Tuple[Optional[bytes], int]]] = [ 50 | None 51 | ] * math.prod(page.chunked) 52 | 53 | x_chunks = page.chunked[page.axes.index("X")] if "X" in page.axes else 1 54 | y_chunks = page.chunked[page.axes.index("Y")] if "Y" in page.axes else 1 55 | z_chunks = page.chunked[page.axes.index("Z")] if "Z" in page.axes else 1 56 | 57 | if keyframe.is_tiled: 58 | tilewidth = keyframe.tilewidth 59 | tilelength = keyframe.tilelength 60 | tiledepth = keyframe.tiledepth 61 | else: 62 | # striped image 63 | tilewidth = keyframe.imagewidth 64 | tilelength = keyframe.rowsperstrip 65 | tiledepth = 1 # TODO: Find 3D striped image to test 66 | 67 | for segment in fh.read_segments( 68 | page.dataoffsets, page.databytecounts, lock=lock, buffersize=buffer_size 69 | ): 70 | x_index = segment[1] % x_chunks 71 | y_index = (segment[1] // x_chunks) % y_chunks 72 | z_index = segment[1] // (y_chunks * x_chunks) 73 | 74 | if z_index >= z_chunks or y_index >= y_chunks or x_index >= x_chunks: 75 | logger.warning( 76 | f"Found segment with index (Z: {z_index},Y: {y_index}, X:{x_index}) outside of bounds. Check for source file corruption." 77 | ) 78 | continue 79 | 80 | segment_cache[segment[1]] = segment 81 | 82 | while (offsets := has_row(segment_cache, page)) is not None: 83 | y_offset, z_offset = offsets 84 | 85 | x_size = keyframe.imagewidth 86 | y_size = min( 87 | keyframe.imagelength - y_offset * tilelength, 88 | tilelength, 89 | ) 90 | z_size = min(keyframe.imagedepth - z_offset * tiledepth, tiledepth) 91 | 92 | buffer = numpy.zeros( 93 | shape=(1, z_size, y_size, x_size, keyframe.samplesperpixel), 94 | dtype=page.dtype, 95 | ) 96 | 97 | for x_offset in range(x_chunks): 98 | idx = z_offset * y_chunks * x_chunks + y_offset * x_chunks + x_offset 99 | data, (_, z, y, x, s), size = keyframe.decode( 100 | *segment_cache[idx], **decodeargs 101 | ) 102 | buffer[0, 0 : size[0], 0 : size[1], x : x + size[2]] = data[ 103 | : keyframe.imagedepth - z, 104 | : keyframe.imagelength - y, 105 | : keyframe.imagewidth - x, 106 | ] 107 | 108 | segment_cache[idx] = None 109 | 110 | shape = (z_size,) if "Z" in page.axes else () 111 | shape = shape + (y_size,) if "Y" in page.axes else shape 112 | shape = shape + (x_size,) if "X" in page.axes else shape 113 | shape = shape + (keyframe.samplesperpixel,) if "S" in page.axes else shape 114 | 115 | offset = (z_offset * tiledepth,) if "Z" in page.axes else () 116 | offset = offset + (y_offset * tilelength,) if "Y" in page.axes else offset 117 | offset = offset + (0,) if "X" in page.axes else offset 118 | offset = offset + (0,) if "S" in page.axes else offset 119 | 120 | yield buffer.reshape(shape), offset 121 | 122 | while (offsets := has_column(segment_cache, page)) is not None: 123 | x_offset, z_offset = offsets 124 | 125 | x_size = min(keyframe.imagewidth - x_offset * tilewidth, tilewidth) 126 | y_size = keyframe.imagelength 127 | z_size = min(keyframe.imagedepth - z_offset * tiledepth, tiledepth) 128 | 129 | buffer = numpy.zeros( 130 | shape=(1, z_size, y_size, x_size, keyframe.samplesperpixel), 131 | dtype=page.dtype, 132 | ) 133 | 134 | for y_offset in range(y_chunks): 135 | idx = z_offset * y_chunks * x_chunks + y_offset * x_chunks + x_offset 136 | data, (_, z, y, x, s), size = keyframe.decode( 137 | *segment_cache[idx], **decodeargs 138 | ) 139 | buffer[0, 0 : size[0], y : y + size[1], 0 : size[2]] = data[ 140 | : keyframe.imagedepth - z, 141 | : keyframe.imagelength - y, 142 | : keyframe.imagewidth - x, 143 | ] 144 | 145 | segment_cache[idx] = None 146 | 147 | shape = (z_size,) if "Z" in page.axes else () 148 | shape = shape + (y_size,) if "Y" in page.axes else shape 149 | shape = shape + (x_size,) if "X" in page.axes else shape 150 | shape = shape + (keyframe.samplesperpixel,) if "S" in page.axes else shape 151 | 152 | offset = (z_offset * tiledepth,) if "Z" in page.axes else () 153 | offset = offset + (0,) if "Y" in page.axes else offset 154 | offset = offset + (x_offset * tilewidth,) if "X" in page.axes else offset 155 | offset = offset + (0,) if "S" in page.axes else offset 156 | 157 | yield buffer.reshape(shape), offset 158 | 159 | while (offsets := has_depth(segment_cache, page)) is not None: 160 | x_offset, y_offset = offsets 161 | 162 | x_size = min(keyframe.imagewidth - x_offset * tilewidth, tilewidth) 163 | y_size = min(keyframe.imagelength - y_offset * tilelength, tilelength) 164 | z_size = keyframe.imagedepth 165 | 166 | buffer = numpy.zeros( 167 | shape=(1, z_size, y_size, x_size, keyframe.samplesperpixel), 168 | dtype=page.dtype, 169 | ) 170 | 171 | for z_offset in range(z_chunks): 172 | idx = z_offset * y_chunks * x_chunks + y_offset * x_chunks + x_offset 173 | data, (_, z, y, x, s), size = keyframe.decode( 174 | *segment_cache[idx], **decodeargs 175 | ) 176 | buffer[0, z : z + size[0], 0 : 0 + size[1], 0 : size[2]] = data[ 177 | : keyframe.imagedepth - z, 178 | : keyframe.imagelength - y, 179 | : keyframe.imagewidth - x, 180 | ] 181 | 182 | segment_cache[idx] = None 183 | 184 | shape = (z_size,) if "Z" in page.axes else () 185 | shape = shape + (y_size,) if "Y" in page.axes else shape 186 | shape = shape + (x_size,) if "X" in page.axes else shape 187 | shape = shape + (keyframe.samplesperpixel,) if "S" in page.axes else shape 188 | 189 | offset = (0,) if "Z" in page.axes else () 190 | offset = offset + (y_offset * tilelength,) if "Y" in page.axes else offset 191 | offset = offset + (x_offset * tilewidth,) if "X" in page.axes else offset 192 | offset = offset + (0,) if "S" in page.axes else offset 193 | 194 | yield buffer.reshape(shape), offset 195 | 196 | 197 | def has_row( 198 | segments: Sequence[Optional[Tuple[Optional[bytes], int]]], page: TiffPage 199 | ) -> Optional[Tuple[int, int]]: 200 | # TODO: Check bitarray for performance improvement 201 | if "X" not in page.axes: 202 | return None 203 | 204 | x_chunks = page.chunked[page.axes.index("X")] if "X" in page.axes else 1 205 | y_chunks = page.chunked[page.axes.index("Y")] if "Y" in page.axes else 1 206 | z_chunks = page.chunked[page.axes.index("Z")] if "Z" in page.axes else 1 207 | 208 | for z_offset in range(z_chunks): 209 | for y_offset in range(y_chunks): 210 | for x_offset in range(x_chunks): 211 | idx = z_offset * x_chunks * y_chunks + y_offset * x_chunks + x_offset 212 | 213 | if segments[idx] is None: 214 | break 215 | else: 216 | return y_offset, z_offset 217 | 218 | return None 219 | 220 | 221 | def has_column( 222 | segments: Sequence[Optional[Tuple[Optional[bytes], int]]], page: TiffPage 223 | ) -> Optional[Tuple[int, int]]: 224 | # TODO: Check bitarray for performance improvement 225 | if "Y" not in page.axes: 226 | return None 227 | 228 | x_chunks = page.chunked[page.axes.index("X")] if "X" in page.axes else 1 229 | y_chunks = page.chunked[page.axes.index("Y")] if "Y" in page.axes else 1 230 | z_chunks = page.chunked[page.axes.index("Z")] if "Z" in page.axes else 1 231 | 232 | for z_offset in range(z_chunks): 233 | for x_offset in range(x_chunks): 234 | for y_offset in range(y_chunks): 235 | idx = z_offset * x_chunks * y_chunks + y_offset * x_chunks + x_offset 236 | 237 | if segments[idx] is None: 238 | break 239 | else: 240 | return x_offset, z_offset 241 | 242 | return None 243 | 244 | 245 | def has_depth( 246 | segments: Sequence[Optional[Tuple[Optional[bytes], int]]], page: TiffPage 247 | ) -> Optional[Tuple[int, int]]: 248 | # TODO: Check bitarray for performance improvement 249 | if "Z" not in page.axes: 250 | return None 251 | 252 | x_chunks = page.chunked[page.axes.index("X")] if "X" in page.axes else 1 253 | y_chunks = page.chunked[page.axes.index("Y")] if "Y" in page.axes else 1 254 | z_chunks = page.chunked[page.axes.index("Z")] if "Z" in page.axes else 1 255 | 256 | for y_offset in range(y_chunks): 257 | for x_offset in range(x_chunks): 258 | for z_offset in range(z_chunks): 259 | idx = z_offset * x_chunks * y_chunks + y_offset * x_chunks + x_offset 260 | 261 | if segments[idx] is None: 262 | break 263 | else: 264 | return x_offset, y_offset 265 | 266 | return None 267 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/metadata.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | import numpy as np 4 | import tifffile 5 | from tifffile import TiffFile, TiffPageSeries 6 | 7 | from tiledb.bioimg.helpers import iter_color 8 | 9 | 10 | def qpi_original_meta(file: TiffFile) -> List[Dict[str, Any]]: 11 | metadata: List[Dict[str, Any]] = [] 12 | 13 | for page in file.pages.pages: 14 | metadata.append({"description": page.description, "tags": page.tags}) 15 | 16 | return metadata 17 | 18 | 19 | def qpi_image_meta(baseline: TiffPageSeries) -> Dict[str, Any]: 20 | # https://downloads.openmicroscopy.org/images/Vectra-QPTIFF/perkinelmer/PKI_Image%20Format.docx 21 | # Read the channel information from the tiff pages 22 | metadata: Dict[str, Any] = { 23 | "channels": [], 24 | "physicalSizeX": (1 / baseline.keyframe.resolution[0]), 25 | "physicalSizeΥ": (1 / baseline.keyframe.resolution[0]), 26 | "physicalSizeΧUnit": "cm", 27 | "physicalSizeΥUnit": "cm", 28 | } 29 | 30 | for idx, page in enumerate(baseline._pages): 31 | page_metadata = tifffile.xml2dict(page.description).get( 32 | "PerkinElmer-QPI-ImageDescription", {} 33 | ) 34 | if page.photometric == tifffile.PHOTOMETRIC.RGB: 35 | color_generator = iter_color(np.dtype(np.uint8), 3) 36 | 37 | metadata["channels"] = [ 38 | {"id": f"{idx}", "name": f"{name}", "color": next(color_generator)} 39 | for idx, name in enumerate(["red", "green", "blue"]) 40 | ] 41 | else: 42 | metadata["channels"].append( 43 | { 44 | "name": page_metadata.get("Name", f"Channel {idx}"), 45 | "id": f"{idx}", 46 | "color": { 47 | name: int(value) 48 | for name, value in zip( 49 | ["red", "green", "blue", "alpha"], 50 | page_metadata.get("Color", "255,255,255").split(",") 51 | + ["255"], 52 | ) 53 | }, 54 | } 55 | ) 56 | 57 | return metadata 58 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/ome_zarr.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | import warnings 6 | from typing import ( 7 | Any, 8 | Dict, 9 | Iterator, 10 | List, 11 | Mapping, 12 | Optional, 13 | Sequence, 14 | Tuple, 15 | cast, 16 | ) 17 | 18 | import numpy 19 | import numpy as np 20 | from numpy._typing import NDArray 21 | 22 | try: 23 | import zarr 24 | from ome_zarr.reader import OMERO, Multiscales, Reader, ZarrLocation 25 | from ome_zarr.writer import write_multiscale 26 | from zarr.codecs import Blosc 27 | except ImportError as err: 28 | warnings.warn( 29 | "OMEZarr Converter requires 'ome-zarr' package. " 30 | "You can install 'tiledb-bioimg' with the 'zarr' or 'full' flag" 31 | ) 32 | raise err 33 | 34 | from tiledb import Config, Ctx 35 | from tiledb.filter import WebpFilter 36 | from tiledb.highlevel import _get_ctx 37 | 38 | from .. import WHITE_RGB 39 | from ..helpers import get_logger_wrapper, get_rgba, translate_config_to_s3fs 40 | from .axes import Axes 41 | from .base import ImageConverterMixin 42 | 43 | 44 | class OMEZarrReader: 45 | 46 | _logger: logging.Logger 47 | 48 | def __init__( 49 | self, 50 | input_path: str, 51 | logger: Optional[logging.Logger] = None, 52 | *, 53 | source_config: Optional[Config] = None, 54 | source_ctx: Optional[Ctx] = None, 55 | dest_config: Optional[Config] = None, 56 | dest_ctx: Optional[Ctx] = None, 57 | ): 58 | """ 59 | OME-Zarr image reader 60 | :param input_path: The path to the Zarr image 61 | """ 62 | self._logger = get_logger_wrapper(False) if not logger else logger 63 | self._source_ctx = _get_ctx(source_ctx, source_config) 64 | self._source_cfg = self._source_ctx.config() 65 | self._dest_ctx = _get_ctx(dest_ctx, dest_config) 66 | self._dest_cfg = self._dest_ctx.config() 67 | storage_options = translate_config_to_s3fs(self._source_cfg) 68 | input_fh = zarr.storage.FSStore( 69 | input_path, check=True, create=True, **storage_options 70 | ) 71 | self._root_node = next(Reader(ZarrLocation(input_fh))()) 72 | self._multiscales = cast(Multiscales, self._root_node.load(Multiscales)) 73 | self._omero = cast(Optional[OMERO], self._root_node.load(OMERO)) 74 | 75 | def __enter__(self) -> OMEZarrReader: 76 | return self 77 | 78 | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 79 | pass 80 | 81 | @property 82 | def source_ctx(self) -> Ctx: 83 | return self._source_ctx 84 | 85 | @property 86 | def dest_ctx(self) -> Ctx: 87 | return self._dest_ctx 88 | 89 | @property 90 | def logger(self) -> Optional[logging.Logger]: 91 | return self._logger 92 | 93 | @property 94 | def axes(self) -> Axes: 95 | axes = Axes(a["name"].upper() for a in self._multiscales.node.metadata["axes"]) 96 | self._logger.debug(f"Reader axes: {axes}") 97 | return axes 98 | 99 | @property 100 | def channels(self) -> Sequence[str]: 101 | return ( 102 | tuple(self._omero.node.metadata.get("channel_names", ())) 103 | if self._omero 104 | else () 105 | ) 106 | 107 | @property 108 | def webp_format(self) -> WebpFilter.WebpInputFormat: 109 | channels = self._omero.image_data.get("channels", ()) if self._omero else () 110 | colors = tuple(channel.get("color") for channel in channels) 111 | self._logger.debug(f"Webp format - channels: {channels}, colors:{colors}") 112 | 113 | if colors == ("FF0000", "00FF00", "0000FF"): 114 | return WebpFilter.WebpInputFormat.WEBP_RGB 115 | return WebpFilter.WebpInputFormat.WEBP_NONE 116 | 117 | @property 118 | def level_count(self) -> int: 119 | level_count = len(self._multiscales.datasets) 120 | self._logger.debug(f"Level count: {level_count}") 121 | return level_count 122 | 123 | def level_dtype(self, level: int) -> np.dtype: 124 | dtype = self._multiscales.node.data[level].dtype 125 | self._logger.debug(f"Level {level} dtype: {dtype}") 126 | return dtype 127 | 128 | def level_shape(self, level: int) -> Tuple[int, ...]: 129 | l_shape = cast(Tuple[int, ...], self._multiscales.node.data[level].shape) 130 | self._logger.debug(f"Level {level} shape: {l_shape}") 131 | return l_shape 132 | 133 | def level_image( 134 | self, level: int, tile: Optional[Tuple[slice, ...]] = None 135 | ) -> np.ndarray: 136 | dask_array = self._multiscales.node.data[level] 137 | if tile is not None: 138 | dask_array = dask_array[tile] 139 | return np.asarray(dask_array) 140 | 141 | def level_metadata(self, level: int) -> Dict[str, Any]: 142 | dataset = self._multiscales.datasets[level] 143 | location = ZarrLocation(self._multiscales.zarr.subpath(dataset)) 144 | self._logger.debug(f"Level {level} - Metadata: {json.dumps(location.zarray)}") 145 | return {"json_zarray": json.dumps(location.zarray)} 146 | 147 | @property 148 | def group_metadata(self) -> Dict[str, Any]: 149 | multiscale = self._multiscales.lookup("multiscales", [])[0] 150 | writer_kwargs = dict( 151 | axes=multiscale.get("axes"), 152 | coordinate_transformations=[ 153 | d.get("coordinateTransformations") for d in multiscale["datasets"] 154 | ], 155 | name=multiscale.get("name"), 156 | metadata=multiscale.get("metadata"), 157 | omero=self._omero.image_data if self._omero else None, 158 | ) 159 | self._logger.debug(f"Group metadata: {writer_kwargs}") 160 | return {"json_zarrwriter_kwargs": json.dumps(writer_kwargs)} 161 | 162 | @property 163 | def image_metadata(self) -> Dict[str, Any]: 164 | # Based on information available at https://ngff.openmicroscopy.org/latest/#metadata 165 | # The start and end values may differ from the channel min-max values as well as the 166 | # min-max values of the metadata. 167 | metadata: Dict[str, Any] = {} 168 | 169 | base_type = self.level_dtype(0) 170 | channel_min = ( 171 | np.iinfo(base_type).min 172 | if np.issubdtype(base_type, numpy.integer) 173 | else np.finfo(base_type).min 174 | ) 175 | channel_max = ( 176 | np.iinfo(base_type).max 177 | if np.issubdtype(base_type, np.integer) 178 | else np.finfo(base_type).max 179 | ) 180 | 181 | metadata["channels"] = [] 182 | 183 | omero_metadata = self._multiscales.lookup("omero", {}) 184 | for idx, channel in enumerate(omero_metadata.get("channels", [])): 185 | metadata["channels"].append( 186 | { 187 | "id": f"{idx}", 188 | "name": channel.get("label", f"Channel:{idx}"), 189 | "color": get_rgba( 190 | int( 191 | channel.get("color", hex(np.random.randint(0, WHITE_RGB))) 192 | + "FF", 193 | base=16, 194 | ) 195 | ), 196 | "min": channel.get("window", {}).get("start", channel_min), 197 | "max": channel.get("window", {}).get("end", channel_max), 198 | } 199 | ) 200 | self._logger.debug(f"Image metadata: {metadata}") 201 | return metadata 202 | 203 | @property 204 | def original_metadata(self) -> Dict[str, Any]: 205 | metadata: Dict[str, Dict[str, Any]] = {"ZARR": {}} 206 | 207 | for key, value in self._root_node.root.zarr.root_attrs.items(): 208 | metadata["ZARR"].setdefault(key, value) 209 | 210 | return metadata 211 | 212 | def optimal_reader( 213 | self, level: int, max_workers: int = 0 214 | ) -> Optional[Iterator[Tuple[Tuple[slice, ...], NDArray[Any]]]]: 215 | return None 216 | 217 | 218 | class OMEZarrWriter: 219 | def __init__(self, output_path: str, logger: logging.Logger, **kwargs: Any) -> None: 220 | """ 221 | OME-Zarr image writer from TileDB 222 | 223 | :param output_path: The path to the Zarr image 224 | """ 225 | self._logger = logger 226 | self._group = zarr.group( 227 | store=zarr.storage.DirectoryStore(path=output_path), overwrite=True 228 | ) 229 | self._pyramid: List[np.ndarray] = [] 230 | self._storage_options: List[Dict[str, Any]] = [] 231 | self._group_metadata: Dict[str, Any] = {} 232 | 233 | def __enter__(self) -> OMEZarrWriter: 234 | return self 235 | 236 | def write_group_metadata(self, metadata: Mapping[str, Any]) -> None: 237 | self._group_metadata = json.loads(metadata["json_zarrwriter_kwargs"]) 238 | 239 | def write_level_image( 240 | self, 241 | image: np.ndarray, 242 | metadata: Mapping[str, Any], 243 | ) -> None: 244 | # store the image to be written at __exit__ 245 | self._pyramid.append(image) 246 | # store the zarray metadata to be written at __exit__ 247 | zarray = dict(metadata) 248 | compressor = zarray["compressor"] 249 | del compressor["id"] 250 | zarray["compressor"] = Blosc.from_config(compressor) 251 | self._storage_options.append(zarray) 252 | 253 | def compute_level_metadata( 254 | self, 255 | baseline: bool, 256 | num_levels: int, 257 | image_dtype: np.dtype, 258 | group_metadata: Mapping[str, Any], 259 | array_metadata: Mapping[str, Any], 260 | **writer_kwargs: Mapping[str, Any], 261 | ) -> Mapping[str, Any]: 262 | return dict(json.loads(array_metadata.get("json_zarray", "{}"))) 263 | 264 | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 265 | group_metadata = self._group_metadata 266 | write_multiscale( 267 | pyramid=self._pyramid, 268 | group=self._group, 269 | axes=group_metadata["axes"], 270 | coordinate_transformations=group_metadata["coordinate_transformations"], 271 | storage_options=self._storage_options, 272 | name=group_metadata["name"], 273 | metadata=group_metadata["metadata"], 274 | ) 275 | if group_metadata["omero"]: 276 | self._group.attrs["omero"] = group_metadata["omero"] 277 | 278 | 279 | class OMEZarrConverter(ImageConverterMixin[OMEZarrReader, OMEZarrWriter]): 280 | """Converter of Zarr-supported images to TileDB Groups of Arrays""" 281 | 282 | _ImageReaderType = OMEZarrReader 283 | _ImageWriterType = OMEZarrWriter 284 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/openslide.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import Any, Dict, Iterator, Optional, Sequence, Tuple, cast 5 | 6 | import numpy as np 7 | import openslide as osd 8 | from numpy._typing import NDArray 9 | 10 | from tiledb import Config, Ctx 11 | from tiledb.filter import WebpFilter 12 | from tiledb.highlevel import _get_ctx 13 | 14 | from ..helpers import cache_filepath, get_logger_wrapper, is_remote_protocol, iter_color 15 | from . import DEFAULT_SCRATCH_SPACE 16 | from .axes import Axes 17 | from .base import ImageConverterMixin 18 | 19 | 20 | class OpenSlideReader: 21 | 22 | _logger: logging.Logger 23 | 24 | def __init__( 25 | self, 26 | input_path: str, 27 | logger: Optional[logging.Logger] = None, 28 | *, 29 | source_config: Optional[Config] = None, 30 | source_ctx: Optional[Ctx] = None, 31 | dest_config: Optional[Config] = None, 32 | dest_ctx: Optional[Ctx] = None, 33 | scratch_space: str = DEFAULT_SCRATCH_SPACE, 34 | ): 35 | """ 36 | OpenSlide image reader 37 | :param input_path: The path to the OpenSlide image 38 | 39 | """ 40 | self._source_ctx = _get_ctx(source_ctx, source_config) 41 | self._source_cfg = self._source_ctx.config() 42 | self._dest_ctx = _get_ctx(dest_ctx, dest_config) 43 | self._dest_cfg = self._dest_ctx.config() 44 | self._logger = get_logger_wrapper(False) if not logger else logger 45 | if is_remote_protocol(input_path): 46 | resolved_path = cache_filepath( 47 | input_path, source_config, source_ctx, self._logger, scratch_space 48 | ) 49 | else: 50 | resolved_path = input_path 51 | self._osd = osd.OpenSlide(resolved_path) 52 | 53 | def __enter__(self) -> OpenSlideReader: 54 | return self 55 | 56 | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 57 | self._osd.close() 58 | 59 | @property 60 | def source_ctx(self) -> Ctx: 61 | return self._source_ctx 62 | 63 | @property 64 | def dest_ctx(self) -> Ctx: 65 | return self._dest_ctx 66 | 67 | @property 68 | def logger(self) -> Optional[logging.Logger]: 69 | return self._logger 70 | 71 | @property 72 | def axes(self) -> Axes: 73 | axes = Axes("YXC") 74 | self._logger.debug(f"Reader axes: {axes}") 75 | return axes 76 | 77 | @property 78 | def channels(self) -> Sequence[str]: 79 | return "RED", "GREEN", "BLUE", "ALPHA" 80 | 81 | @property 82 | def webp_format(self) -> WebpFilter.WebpInputFormat: 83 | self._logger.debug(f"Webp Input Format: {WebpFilter.WebpInputFormat.WEBP_RGBA}") 84 | return WebpFilter.WebpInputFormat.WEBP_RGBA 85 | 86 | @property 87 | def level_count(self) -> int: 88 | level_count = cast(int, self._osd.level_count) 89 | self._logger.debug(f"Level count: {level_count}") 90 | return level_count 91 | 92 | def level_dtype(self, level: int) -> np.dtype: 93 | dtype = np.dtype(np.uint8) 94 | self._logger.debug(f"Level {level} dtype: {dtype}") 95 | return dtype 96 | 97 | def level_shape(self, level: int) -> Tuple[int, ...]: 98 | width, height = self._osd.level_dimensions[level] 99 | # OpenSlide.read_region() returns a PIL image in RGBA mode 100 | # passing it to np.asarray() returns a (height, width, 4) array 101 | # https://stackoverflow.com/questions/49084846/why-different-size-when-converting-pil-image-to-numpy-array 102 | self._logger.debug(f"Level {level} shape: ({width}, {height}, 4)") 103 | return height, width, 4 104 | 105 | def level_image( 106 | self, level: int, tile: Optional[Tuple[slice, ...]] = None 107 | ) -> np.ndarray: 108 | level_size = self._osd.level_dimensions[level] 109 | if tile is None: 110 | location = (0, 0) 111 | size = level_size 112 | else: 113 | # tile: (Y slice, X slice, C slice) 114 | y, x, _ = tile 115 | full_size = self._osd.level_dimensions[0] 116 | # XXX: This is not 100% accurate if the level downsample factors are not integer 117 | # See https://github.com/openslide/openslide/issues/256 118 | location = ( 119 | x.start * round(full_size[0] / level_size[0]), 120 | y.start * round(full_size[1] / level_size[1]), 121 | ) 122 | size = (x.stop - x.start, y.stop - y.start) 123 | return np.asarray(self._osd.read_region(location, level, size)) 124 | 125 | def level_metadata(self, level: int) -> Dict[str, Any]: 126 | self._logger.debug(f"Level {level} - Metadata: None") 127 | return {} 128 | 129 | @property 130 | def group_metadata(self) -> Dict[str, Any]: 131 | self._logger.debug("Group metadata: None") 132 | return {} 133 | 134 | @property 135 | def image_metadata(self) -> Dict[str, Any]: 136 | metadata: Dict[str, Any] = {} 137 | color_generator = iter_color(np.dtype(np.uint8), 3) 138 | properties = self._osd.properties 139 | 140 | # We skip the alpha channel 141 | metadata["channels"] = [ 142 | {"id": f"{idx}", "name": f"{name}", "color": next(color_generator)} 143 | for idx, name in enumerate(["red", "green", "blue"]) 144 | ] 145 | 146 | if "aperio.MPP" in properties: 147 | metadata["physicalSizeX"] = metadata["physicalSizeY"] = float( 148 | properties["aperio.MPP"] 149 | ) 150 | metadata["physicalSizeYUnit"] = metadata["physicalSizeYUnit"] = "µm" 151 | 152 | self._logger.debug(f"Image metadata: {metadata}") 153 | return metadata 154 | 155 | @property 156 | def original_metadata(self) -> Dict[str, Any]: 157 | return {"SVS": list(self._osd.properties.items())} 158 | 159 | def optimal_reader( 160 | self, level: int, max_workers: int = 0 161 | ) -> Optional[Iterator[Tuple[Tuple[slice, ...], NDArray[Any]]]]: 162 | return None 163 | 164 | 165 | class OpenSlideConverter(ImageConverterMixin[OpenSlideReader, Any]): 166 | """Converter of OpenSlide-supported images to TileDB Groups of Arrays""" 167 | 168 | _ImageReaderType = OpenSlideReader 169 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/png.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | from functools import partial 6 | from typing import ( 7 | Any, 8 | Dict, 9 | Iterator, 10 | Mapping, 11 | Optional, 12 | Sequence, 13 | Tuple, 14 | ) 15 | 16 | import numpy as np 17 | from numpy._typing import NDArray 18 | from PIL import Image 19 | 20 | from tiledb import VFS, Config, Ctx 21 | from tiledb.filter import WebpFilter 22 | from tiledb.highlevel import _get_ctx 23 | 24 | from ..helpers import get_logger_wrapper, iter_color 25 | from .axes import Axes 26 | from .base import ImageConverterMixin 27 | 28 | 29 | class PNGReader: 30 | 31 | _logger: logging.Logger 32 | 33 | def __init__( 34 | self, 35 | input_path: str, 36 | logger: Optional[logging.Logger] = None, 37 | *, 38 | source_config: Optional[Config] = None, 39 | source_ctx: Optional[Ctx] = None, 40 | dest_config: Optional[Config] = None, 41 | dest_ctx: Optional[Ctx] = None, 42 | ): 43 | 44 | self._logger = get_logger_wrapper(False) if not logger else logger 45 | self._input_path = input_path 46 | self._source_ctx = _get_ctx(source_ctx, source_config) 47 | self._source_cfg = self._source_ctx.config() 48 | self._dest_ctx = _get_ctx(dest_ctx, dest_config) 49 | self._dest_cfg = self._dest_ctx.config() 50 | self._vfs = VFS(config=self._source_cfg, ctx=self._source_ctx) 51 | self._vfs_fh = self._vfs.open(input_path, mode="rb") 52 | self._png = Image.open(self._vfs_fh) 53 | 54 | # Get initial metadata 55 | self._metadata: Dict[str, Any] = self._png.info 56 | self._metadata.update(dict(self._png.getexif())) 57 | 58 | # Handle all different modes as RGB for consistency 59 | self._metadata["original_mode"] = self._png.mode 60 | if self._png.mode not in ["RGB", "RGBA", "L"]: 61 | self._png = self._png.convert("RGB") 62 | 63 | def __enter__(self) -> PNGReader: 64 | return self 65 | 66 | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 67 | self._vfs.close(file=self._vfs_fh) 68 | 69 | @property 70 | def source_ctx(self) -> Ctx: 71 | return self._source_ctx 72 | 73 | @property 74 | def dest_ctx(self) -> Ctx: 75 | return self._dest_ctx 76 | 77 | @property 78 | def logger(self) -> Optional[logging.Logger]: 79 | return self._logger 80 | 81 | @property 82 | def axes(self) -> Axes: 83 | if self._png.mode == "L": 84 | axes = Axes(["X", "Y"]) 85 | else: 86 | axes = Axes(["X", "Y", "C"]) 87 | self._logger.debug(f"Reader axes: {axes}") 88 | return axes 89 | 90 | @property 91 | def channels(self) -> Sequence[str]: 92 | if self.webp_format is WebpFilter.WebpInputFormat.WEBP_RGB: 93 | self._logger.debug(f"Webp format: {WebpFilter.WebpInputFormat.WEBP_RGB}") 94 | return "RED", "GREEN", "BLUE" 95 | elif self.webp_format is WebpFilter.WebpInputFormat.WEBP_RGBA: 96 | self._logger.debug(f"Webp format: {WebpFilter.WebpInputFormat.WEBP_RGBA}") 97 | return "RED", "GREEN", "BLUE", "ALPHA" 98 | else: 99 | self._logger.debug( 100 | f"Webp format is not: {WebpFilter.WebpInputFormat.WEBP_RGB} / {WebpFilter.WebpInputFormat.WEBP_RGBA}" 101 | ) 102 | color_map = { 103 | "R": "RED", 104 | "G": "GREEN", 105 | "B": "BLUE", 106 | "A": "ALPHA", 107 | "L": "GRAYSCALE", 108 | } 109 | # Use list comprehension to convert the short form to full form 110 | rgb_full = [color_map[color] for color in self._png.getbands()] 111 | return rgb_full 112 | 113 | @property 114 | def level_count(self) -> int: 115 | level_count = 1 116 | self._logger.debug(f"Level count: {level_count}") 117 | return level_count 118 | 119 | def level_dtype(self, level: int = 0) -> np.dtype: 120 | dtype = np.uint8 121 | self._logger.debug(f"Level {level} dtype: {dtype}") 122 | return dtype 123 | 124 | def level_shape(self, level: int = 0) -> Tuple[int, ...]: 125 | if level != 0: 126 | return () 127 | # Even after converting to RGB the size is not updated to 3d from 2d 128 | w, h = self._png.size 129 | 130 | # Numpy shape is of the format (H, W, D) compared to Pillow (W, H, D) 131 | l_shape: Tuple[Any, ...] = () 132 | if self._png.mode == "L": 133 | # Grayscale has 1 channel 134 | l_shape = (h, w) 135 | elif self._png.mode == "RGBA": 136 | # RGB has 4 channels 137 | l_shape = (h, w, 4) 138 | else: 139 | # RGB has 3 channels 140 | l_shape = (h, w, 3) 141 | self._logger.debug(f"Level {level} shape: {l_shape}") 142 | return l_shape 143 | 144 | @property 145 | def webp_format(self) -> WebpFilter.WebpInputFormat: 146 | self._logger.debug(f"Channel Mode: {self._png.mode}") 147 | if self._png.mode == "RGB": 148 | return WebpFilter.WebpInputFormat.WEBP_RGB 149 | elif self._png.mode == "RGBA": 150 | return WebpFilter.WebpInputFormat.WEBP_RGBA 151 | return WebpFilter.WebpInputFormat.WEBP_NONE 152 | 153 | def level_image( 154 | self, level: int = 0, tile: Optional[Tuple[slice, ...]] = None 155 | ) -> np.ndarray: 156 | 157 | if tile is None: 158 | return np.asarray(self._png) 159 | else: 160 | return np.asarray(self._png)[tile] 161 | 162 | def level_metadata(self, level: int) -> Dict[str, Any]: 163 | # Common with group metadata since there are no multiple levels 164 | writer_kwargs = dict(metadata=self._metadata) 165 | return {"json_write_kwargs": json.dumps(writer_kwargs)} 166 | 167 | @property 168 | def group_metadata(self) -> Dict[str, Any]: 169 | writer_kwargs = dict(metadata=self._metadata) 170 | self._logger.debug(f"Group metadata: {writer_kwargs}") 171 | return {"json_write_kwargs": json.dumps(writer_kwargs)} 172 | 173 | @property 174 | def image_metadata(self) -> Dict[str, Any]: 175 | self._logger.debug(f"Image metadata: {self._metadata}") 176 | color_generator = iter_color(np.dtype(np.uint8), len(self.channels)) 177 | 178 | channels = [] 179 | for idx, channel in enumerate(self.channels): 180 | channel_metadata = { 181 | "id": f"{idx}", 182 | "name": f"Channel {idx}", 183 | "color": (next(color_generator)), 184 | } 185 | channels.append(channel_metadata) 186 | self._metadata["channels"] = channels 187 | return self._metadata 188 | 189 | @property 190 | def original_metadata(self) -> Any: 191 | self._logger.debug(f"Original Image metadata: {self._metadata}") 192 | return self._metadata 193 | 194 | def optimal_reader( 195 | self, level: int, max_workers: int = 0 196 | ) -> Optional[Iterator[Tuple[Tuple[slice, ...], NDArray[Any]]]]: 197 | return None 198 | 199 | 200 | class PNGWriter: 201 | 202 | def __init__(self, output_path: str, logger: logging.Logger, **kwargs: Any) -> None: 203 | self._logger = logger 204 | self._output_path = output_path 205 | self._group_metadata: Dict[str, Any] = {} 206 | self._writer = partial(Image.fromarray) 207 | 208 | def __enter__(self) -> PNGWriter: 209 | return self 210 | 211 | def compute_level_metadata( 212 | self, 213 | baseline: bool, 214 | num_levels: int, 215 | image_dtype: np.dtype, 216 | group_metadata: Mapping[str, Any], 217 | array_metadata: Mapping[str, Any], 218 | **writer_kwargs: Mapping[str, Any], 219 | ) -> Mapping[str, Any]: 220 | 221 | writer_metadata: Dict[str, Any] = {} 222 | original_mode = group_metadata.get("original_mode", "RGB") 223 | writer_metadata["mode"] = original_mode 224 | self._logger.debug(f"Writer metadata: {writer_metadata}") 225 | return writer_metadata 226 | 227 | def write_group_metadata(self, metadata: Mapping[str, Any]) -> None: 228 | self._group_metadata = json.loads(metadata["json_write_kwargs"]) 229 | 230 | def write_level_image( 231 | self, 232 | image: np.ndarray, 233 | metadata: Mapping[str, Any], 234 | ) -> None: 235 | 236 | if metadata["mode"] not in ("RGB", "RGBA", "L"): 237 | array_img = self._writer(image, mode="RGB") 238 | else: 239 | array_img = self._writer(image, mode=metadata["mode"]) 240 | original_img = array_img.convert(metadata["mode"]) 241 | original_img.save(self._output_path, format="png") 242 | 243 | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 244 | pass 245 | 246 | 247 | class PNGConverter(ImageConverterMixin[PNGReader, PNGWriter]): 248 | """Converter of Tiff-supported images to TileDB Groups of Arrays""" 249 | 250 | _ImageReaderType = PNGReader 251 | _ImageWriterType = PNGWriter 252 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/scale.py: -------------------------------------------------------------------------------- 1 | from concurrent import futures 2 | from typing import Any, Dict, Mapping, Optional, Sequence, Tuple 3 | 4 | import skimage as sk 5 | 6 | import tiledb 7 | 8 | from .axes import AxesMapper 9 | from .tiles import iter_tiles 10 | 11 | 12 | class Scaler(object): 13 | def __init__( 14 | self, 15 | base_shape: Tuple[int, ...], 16 | base_axes: str, 17 | scale_factors: Sequence[float], 18 | scale_axes: str = "XY", 19 | chunked: bool = False, 20 | progressive: bool = False, 21 | order: int = 1, 22 | max_workers: Optional[int] = None, 23 | ): 24 | self._chunked = chunked 25 | self._progressive = progressive 26 | self._resize_kwargs = dict(order=order, preserve_range=True, anti_aliasing=True) 27 | self._executor = ( 28 | futures.ProcessPoolExecutor(max_workers) if max_workers != 0 else None 29 | ) 30 | self._level_shapes = [] 31 | self._scale_factors = [] 32 | self._scale_compressors: Dict[int, tiledb.Filter] = {} 33 | previous_scale_factor = 1.0 34 | for scale_factor in scale_factors: 35 | dim_factors = [ 36 | scale_factor if axis in scale_axes else 1 for axis in base_axes 37 | ] 38 | self._level_shapes.append( 39 | tuple( 40 | round(dim_size / dim_factor) 41 | for dim_size, dim_factor in zip(base_shape, dim_factors) 42 | ) 43 | ) 44 | if chunked: 45 | if progressive: 46 | dim_factors = [ 47 | ( 48 | scale_factor / previous_scale_factor 49 | if axis in scale_axes 50 | else 1 51 | ) 52 | for axis in base_axes 53 | ] 54 | previous_scale_factor = scale_factor 55 | self._scale_factors.append(dim_factors) 56 | 57 | @property 58 | def level_shapes(self) -> Sequence[Tuple[int, ...]]: 59 | return self._level_shapes 60 | 61 | @property 62 | def chunked(self) -> bool: 63 | return self._chunked 64 | 65 | @property 66 | def progressive(self) -> bool: 67 | return self._progressive 68 | 69 | @property 70 | def compressors(self) -> Mapping[int, tiledb.Filter]: 71 | return self._scale_compressors 72 | 73 | def update_compressors(self, level: int, lvl_filter: tiledb.Filter) -> None: 74 | self._scale_compressors[level] = lvl_filter 75 | 76 | def apply( 77 | self, 78 | in_array: tiledb.Array, 79 | out_array: tiledb.Array, 80 | level: int, 81 | axes_mapper: AxesMapper, 82 | ) -> None: 83 | scale_kwargs = dict( 84 | in_array=in_array, 85 | out_array=out_array, 86 | axes_mapper=axes_mapper, 87 | scale_factors=self._scale_factors[level] if self._scale_factors else None, 88 | **self._resize_kwargs, 89 | ) 90 | 91 | if not self._chunked: 92 | _scale(**scale_kwargs) 93 | elif self._executor: 94 | fs = [ 95 | self._executor.submit(_scale, tile=tile, **scale_kwargs) 96 | for tile in iter_tiles(out_array.domain) 97 | ] 98 | futures.wait(fs) 99 | else: 100 | for tile in iter_tiles(out_array.domain): 101 | _scale(tile=tile, **scale_kwargs) 102 | 103 | 104 | def _scale( 105 | in_array: tiledb.Array, 106 | out_array: tiledb.Array, 107 | axes_mapper: AxesMapper, 108 | tile: Optional[Tuple[slice, ...]] = None, 109 | scale_factors: Sequence[float] = (), 110 | **resize_kwargs: Any, 111 | ) -> None: 112 | if tile is None: 113 | tile = axes_mapper.inverse.map_tile( 114 | tuple(slice(0, size) for size in out_array.shape) 115 | ) 116 | image = in_array[:] 117 | else: 118 | tile = axes_mapper.inverse.map_tile(tile) 119 | 120 | scaled_tile = [] 121 | in_shape = in_array.shape 122 | assert ( 123 | len(tile) 124 | == len(scale_factors) 125 | == len(axes_mapper.inverse.map_shape(in_shape)) 126 | ) 127 | for tile_slice, scale_factor, dim_size in zip( 128 | tile, scale_factors, axes_mapper.inverse.map_shape(in_shape) 129 | ): 130 | start = int(tile_slice.start * scale_factor) 131 | stop = int(min(tile_slice.stop * scale_factor, dim_size)) 132 | scaled_tile.append(slice(start, stop)) 133 | image = in_array[tuple(axes_mapper.map_tile(tuple(scaled_tile)))] 134 | 135 | tile_shape = tuple(s.stop - s.start for s in tile) 136 | tile = axes_mapper.map_tile(tile) 137 | 138 | out_array[tuple(tile)] = axes_mapper.map_array( 139 | sk.transform.resize( 140 | axes_mapper.inverse.map_array(image), tile_shape, **resize_kwargs 141 | ) 142 | ) 143 | -------------------------------------------------------------------------------- /tiledb/bioimg/converters/tiles.py: -------------------------------------------------------------------------------- 1 | import itertools as it 2 | from typing import Iterator, MutableSequence, Sequence, Tuple, Union 3 | 4 | import tiledb 5 | 6 | 7 | def iter_tiles( 8 | domain: Union[tiledb.Domain, Sequence[Tuple[int, int, int]]], scale: int = 1 9 | ) -> Iterator[Tuple[slice, ...]]: 10 | transformed_domain: MutableSequence[Tuple[int, int, int]] = [] 11 | 12 | if isinstance(domain, tiledb.Domain): 13 | for dim in domain: 14 | transformed_domain.append( 15 | (int(dim.domain[0]), int(dim.domain[1]), int(dim.tile) * scale) 16 | ) 17 | else: 18 | for dim in domain: 19 | transformed_domain.append((dim[0], dim[1], dim[2] * scale)) 20 | 21 | """Generate all the non-overlapping tiles that cover the given TileDB domain.""" 22 | return it.product(*map(iter_slices, map(dim_range, transformed_domain))) 23 | 24 | 25 | def num_tiles( 26 | domain: Union[tiledb.Domain, Sequence[Tuple[int, int, int]]], scale: int = 1 27 | ) -> int: 28 | """Compute the number of non-overlapping tiles that cover the given TileDB domain.""" 29 | transformed_domain: MutableSequence[Tuple[int, int, int]] = [] 30 | 31 | if isinstance(domain, tiledb.Domain): 32 | for dim in domain: 33 | transformed_domain.append( 34 | (int(dim.domain[0]), int(dim.domain[1]), int(dim.tile) * scale) 35 | ) 36 | else: 37 | for dim in domain: 38 | transformed_domain.append((dim[0], dim[1], dim[2] * scale)) 39 | 40 | n = 1 41 | for dim in transformed_domain: 42 | n *= len(dim_range(dim)) 43 | return n 44 | 45 | 46 | def dim_range(dim: Tuple[int, int, int]) -> range: 47 | """Get the range of the given tiledb dimension with step equal to the dimension tile.""" 48 | return range(dim[0], dim[1] + 1, dim[2]) 49 | 50 | 51 | def iter_slices(r: range) -> Iterator[slice]: 52 | """ 53 | Generate all the non-overlapping slices that cover the given range `r`, 54 | with each slice having length `r.step` (except possibly the last one). 55 | 56 | slice(r[0], r[1]) 57 | slice(r[1], r[2]) 58 | ... 59 | slice(r[n-2], r[n-1]) 60 | slice(r[n-1], r.stop) 61 | """ 62 | yield from it.starmap(slice, zip(r, r[1:])) 63 | yield slice(r[-1], r.stop) 64 | -------------------------------------------------------------------------------- /tiledb/bioimg/openslide.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import warnings 4 | from operator import attrgetter 5 | from typing import Any, Mapping, MutableMapping, Optional, Sequence, Tuple, Union 6 | 7 | import numpy as np 8 | 9 | try: 10 | import dask.array as da 11 | except ImportError: 12 | pass 13 | 14 | import json 15 | 16 | import tiledb 17 | from tiledb import Config, Ctx 18 | from tiledb.highlevel import _get_ctx 19 | 20 | from . import ATTR_NAME 21 | from .converters.axes import Axes 22 | from .helpers import open_bioimg 23 | 24 | 25 | class TileDBOpenSlide: 26 | @classmethod 27 | def from_group_uri(cls, uri: str, attr: str = ATTR_NAME) -> TileDBOpenSlide: 28 | warnings.warn( 29 | "This method is deprecated, please use TileDBOpenSlide() instead", 30 | DeprecationWarning, 31 | stacklevel=2, 32 | ) 33 | return cls(uri, attr=attr) 34 | 35 | def __init__( 36 | self, 37 | uri: str, 38 | *, 39 | attr: str = ATTR_NAME, 40 | config: Config = None, 41 | ctx: Optional[Ctx] = None, 42 | ): 43 | """Open this TileDBOpenSlide. 44 | 45 | :param uri: uri of a tiledb.Group containing the image 46 | """ 47 | self._ctx = _get_ctx(ctx, config) 48 | self._cfg = self._ctx.config() 49 | self._group = tiledb.Group(uri, ctx=self._ctx) 50 | pixel_depth = self._group.meta.get("pixel_depth", "") 51 | pixel_depth = dict(json.loads(pixel_depth)) if pixel_depth else {} 52 | self._levels = sorted( 53 | ( 54 | TileDBOpenSlideLevel( 55 | o.uri, pixel_depth, attr=attr, config=config, ctx=ctx 56 | ) 57 | for o in self._group 58 | ), 59 | key=attrgetter("level"), 60 | ) 61 | 62 | def __enter__(self) -> TileDBOpenSlide: 63 | return self 64 | 65 | def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: 66 | for level in self._levels: 67 | level.close() 68 | self._group.close() 69 | 70 | @property 71 | def level_count(self) -> int: 72 | """Number of levels in the slide""" 73 | return len(self._levels) 74 | 75 | @property 76 | def levels(self) -> Sequence[int]: 77 | """Sequence of level numbers in the slide. 78 | 79 | Levels are numbered from `level_min` (highest resolution) to `level_count - 1` 80 | (lowest resolution), where `level_min` is the value of the respective 81 | `ImageConverter.to_tiledb` parameter (default=0) when creating the slide. 82 | """ 83 | return tuple(map(attrgetter("level"), self._levels)) 84 | 85 | @property 86 | def dimensions(self) -> Tuple[int, int]: 87 | """A (width, height) tuple for level 0 of the slide.""" 88 | return self._levels[0].dimensions 89 | 90 | @property 91 | def level_dimensions(self) -> Sequence[Tuple[int, int]]: 92 | """ 93 | A sequence of (width, height) tuples, one for each level of the slide. 94 | level_dimensions[k] are the dimensions of level k. 95 | 96 | :return: A sequence of dimensions for each level 97 | """ 98 | return tuple(map(attrgetter("dimensions"), self._levels)) 99 | 100 | @property 101 | def level_downsamples(self) -> Sequence[float]: 102 | """ 103 | A sequence of downsample factors for each level of the slide. 104 | level_downsamples[k] is the downsample factor of level k. 105 | """ 106 | level_dims = self.level_dimensions 107 | l0_w, l0_h = level_dims[0] 108 | return tuple((l0_w / w + l0_h / h) / 2.0 for w, h in level_dims) 109 | 110 | @property 111 | def properties(self) -> Mapping[str, Any]: 112 | """Metadata about the slide""" 113 | return dict(self._group.meta) 114 | 115 | def level_properties(self, level: int) -> Mapping[str, Any]: 116 | """Metadata about the given slide level""" 117 | return self._levels[level].properties 118 | 119 | def read_level(self, level: int, to_original_axes: bool = False) -> np.ndarray: 120 | """ 121 | Return an image containing the contents of the specified level as NumPy array. 122 | 123 | :param level: the level number 124 | :param to_original_axes: If True return the image in the original axes, 125 | otherwise return it in YXC (height, width, channel) axes. 126 | """ 127 | return self._read_image(level, to_original_axes=to_original_axes) 128 | 129 | def read_level_dask(self, level: int, to_original_axes: bool = False) -> da.Array: 130 | """ 131 | Return an image containing the contents of the specified level as Dask array. 132 | 133 | :param level: the level number 134 | :param to_original_axes: If True return the image in the original axes, 135 | otherwise return it in YXC (height, width, channel) axes. 136 | """ 137 | return self._read_image(level, to_original_axes=to_original_axes, to_dask=True) 138 | 139 | def read_region( 140 | self, location: Tuple[int, int], level: int, size: Tuple[int, int] 141 | ) -> np.ndarray: 142 | """ 143 | Return an image containing the contents of the specified region as NumPy array. 144 | 145 | :param location: (x, y) tuple giving the top left pixel in the level 0 reference frame 146 | :param level: the level number 147 | :param size: (width, height) tuple giving the region size 148 | 149 | :return: 3D (height, width, channel) Numpy array 150 | """ 151 | x, y = location 152 | w, h = size 153 | return self._read_image(level, {"X": slice(x, x + w), "Y": slice(y, y + h)}) 154 | 155 | def get_best_level_for_downsample(self, factor: float) -> int: 156 | """Return the best level for displaying the given downsample filtering by factor. 157 | 158 | :param factor: The factor of downsamples. Above this value downsamples are filtered out. 159 | 160 | :return: The number corresponding to a level 161 | """ 162 | lla = np.where(np.array(self.level_downsamples) < factor)[0] 163 | return int(lla.max() if len(lla) > 0 else 0) 164 | 165 | def _read_image( 166 | self, 167 | level: int, 168 | dim_slice: MutableMapping[str, slice] = {}, 169 | to_original_axes: bool = False, 170 | to_dask: bool = False, 171 | ) -> Union[np.ndarray, da.Array]: 172 | axes = Axes(self._group.meta["axes"] if to_original_axes else "YXC") 173 | return self._levels[level].read(axes, dim_slice, to_dask) 174 | 175 | 176 | class TileDBOpenSlideLevel: 177 | def __init__( 178 | self, 179 | uri: str, 180 | pixel_depth: Mapping[str, int], 181 | *, 182 | attr: str, 183 | config: Config = None, 184 | ctx: Optional[Ctx] = None, 185 | ): 186 | self._tdb = open_bioimg(uri, attr=attr, config=config, ctx=ctx) 187 | self._pixel_depth = pixel_depth.get(str(self.level), 1) 188 | 189 | @property 190 | def level(self) -> int: 191 | return int(self._tdb.meta["level"]) 192 | 193 | @property 194 | def dimensions(self) -> Tuple[int, int]: 195 | a = self._tdb 196 | dims = list(a.domain) 197 | width = a.shape[dims.index(a.dim("X"))] 198 | height = a.shape[dims.index(a.dim("Y"))] 199 | return width // self._pixel_depth, height 200 | 201 | @property 202 | def properties(self) -> Mapping[str, Any]: 203 | return dict(self._tdb.meta) 204 | 205 | def read( 206 | self, 207 | axes: Axes, 208 | dim_slice: MutableMapping[str, slice] = {}, 209 | to_dask: bool = False, 210 | ) -> Union[np.ndarray, da.Array]: 211 | dims = tuple(dim.name for dim in self._tdb.domain) 212 | pixel_depth = self._pixel_depth 213 | if pixel_depth == 1: 214 | axes_mapper = axes.mapper(Axes(dims)) 215 | else: 216 | x = dim_slice.get("X") 217 | if x is not None: 218 | dim_slice["X"] = slice(x.start * pixel_depth, x.stop * pixel_depth) 219 | axes_mapper = axes.webp_mapper(pixel_depth) 220 | 221 | array = da.from_tiledb(self._tdb) if to_dask else self._tdb 222 | selector = tuple(dim_slice.get(dim, slice(None)) for dim in dims) 223 | return axes_mapper.inverse.map_array(array[selector]) 224 | 225 | def close(self) -> None: 226 | self._tdb.close() 227 | -------------------------------------------------------------------------------- /tiledb/bioimg/plugin_manager.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import Any, Mapping, Type 3 | 4 | from .converters.base import ImageConverterMixin, ImageReader, ImageWriter 5 | 6 | 7 | def _load_entrypoints(name: str) -> Mapping[str, Any]: 8 | if sys.version_info < (3, 12): 9 | import importlib 10 | 11 | eps = importlib.metadata.entry_points()[name] 12 | else: 13 | from importlib.metadata import entry_points 14 | 15 | eps = entry_points(group=name) 16 | return {ep.name: ep.load() for ep in eps} 17 | 18 | 19 | def load_readers() -> Mapping[str, ImageReader]: 20 | return _load_entrypoints("bioimg.readers") 21 | 22 | 23 | def load_writers() -> Mapping[str, ImageWriter]: 24 | return _load_entrypoints("bioimg.writers") 25 | 26 | 27 | def load_converters() -> Mapping[str, Type[ImageConverterMixin[Any, Any]]]: 28 | return _load_entrypoints("bioimg.converters") 29 | -------------------------------------------------------------------------------- /tiledb/bioimg/types.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class Converters(enum.Enum): 5 | OMETIFF = enum.auto() 6 | OMEZARR = enum.auto() 7 | OSD = enum.auto() 8 | PNG = enum.auto() 9 | -------------------------------------------------------------------------------- /tiledb/bioimg/wrappers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib.util 4 | from typing import Any, Callable, Optional, Union 5 | 6 | try: 7 | importlib.util.find_spec("tifffile") 8 | importlib.util.find_spec("imagecodecs") 9 | except ImportError as err_tiff: 10 | _tiff_exc: Optional[ImportError] = err_tiff 11 | else: 12 | _tiff_exc = None 13 | try: 14 | importlib.util.find_spec("zarr") 15 | importlib.util.find_spec("ome-zarr") 16 | except ImportError as err_zarr: 17 | _zarr_exc: Optional[ImportError] = err_zarr 18 | else: 19 | _zarr_exc = None 20 | 21 | from . import _osd_exc 22 | from .helpers import get_logger_wrapper 23 | from .plugin_manager import load_converters 24 | from .types import Converters 25 | 26 | 27 | def from_bioimg( 28 | src: str, 29 | dest: str, 30 | converter: Converters = Converters.OMETIFF, 31 | *, 32 | verbose: bool = False, 33 | exclude_metadata: Union[bool, Callable[[str], str], None] = None, 34 | tile_scale: int = 1, 35 | **kwargs: Any, 36 | ) -> Any: 37 | """ 38 | This function is a wrapper and serves as an all-inclusive API for encapsulating the 39 | ingestion of different file formats 40 | :param src: The source path for the file to be ingested *.tiff, *.zarr, *.svs etc.. 41 | :param dest: The destination path where the TileDB image will be stored 42 | :param converter: The converter type to be used (tentative) soon automatically detected 43 | :param verbose: verbose logging, defaults to False 44 | :param exclude_metadata: An optional argument that specifies how to transform the original metadata. 45 | It can be one of the following: 46 | * A callable (function, method, etc.) that takes an OME-XML string and returns it as a string, while removing 47 | some of the original metadata and excluding them from being ingested. 48 | * A boolean value: 49 | * ``True``: Indicates a specific built-in transformation should be applied. see: `remove_ome_image_metadata` 50 | * ``False``: Indicates no transformation should be applied. 51 | * ``None``: Indicates no transformation should be applied (same as ``False``). 52 | :param kwargs: keyword arguments for custom ingestion behaviour 53 | :return: The converter class that was used for the ingestion 54 | """ 55 | 56 | logger = get_logger_wrapper(verbose) 57 | reader_kwargs = kwargs.get("reader_kwargs", {}) 58 | 59 | # Get the config for the source 60 | reader_kwargs["source_config"] = kwargs.pop("source_config", None) 61 | 62 | # Get the config for the destination (if exists) otherwise match it with source config 63 | reader_kwargs["dest_config"] = kwargs.pop( 64 | "dest_config", reader_kwargs["source_config"] 65 | ) 66 | converters = load_converters() 67 | if converter is Converters.OMETIFF: 68 | if not _tiff_exc: 69 | logger.info("Converting OME-TIFF file") 70 | return converters["tiff_converter"].to_tiledb( 71 | source=src, 72 | output_path=dest, 73 | log=logger, 74 | exclude_metadata=exclude_metadata, 75 | tile_scale=tile_scale, 76 | reader_kwargs=reader_kwargs, 77 | **kwargs, 78 | ) 79 | else: 80 | raise _tiff_exc 81 | elif converter is Converters.OMEZARR: 82 | if not _zarr_exc: 83 | logger.info("Converting OME-Zarr file") 84 | return converters["zarr_converter"].to_tiledb( 85 | source=src, 86 | output_path=dest, 87 | log=logger, 88 | exclude_metadata=exclude_metadata, 89 | tile_scale=tile_scale, 90 | reader_kwargs=reader_kwargs, 91 | **kwargs, 92 | ) 93 | else: 94 | raise _zarr_exc 95 | elif converter is Converters.OSD: 96 | if not _osd_exc: 97 | logger.info("Converting Openslide") 98 | return converters["osd_converter"].to_tiledb( 99 | source=src, 100 | output_path=dest, 101 | log=logger, 102 | exclude_metadata=exclude_metadata, 103 | tile_scale=tile_scale, 104 | reader_kwargs=reader_kwargs, 105 | **kwargs, 106 | ) 107 | else: 108 | raise _osd_exc 109 | else: 110 | 111 | logger.info("Converting PNG") 112 | return converters["png_converter"].to_tiledb( 113 | source=src, 114 | output_path=dest, 115 | log=logger, 116 | exclude_metadata=exclude_metadata, 117 | tile_scale=tile_scale, 118 | reader_kwargs=reader_kwargs, 119 | **kwargs, 120 | ) 121 | 122 | 123 | def to_bioimg( 124 | src: str, 125 | dest: str, 126 | converter: Converters = Converters.OMETIFF, 127 | *, 128 | verbose: bool = False, 129 | **kwargs: Any, 130 | ) -> Any: 131 | """ 132 | This function is a wrapper and serves as an all-inclusive API for encapsulating the 133 | exportation of TileDB ingested bio-images back into different file formats 134 | :param src: The source path where the TileDB image is stored 135 | :param dest: The destination path for the image file to be exported *.tiff, *.zarr, *.svs etc.. 136 | :param converter: The converter type to be used 137 | :param verbose: verbose logging, defaults to False 138 | :param kwargs: keyword arguments for custom exportation behaviour 139 | :return: None 140 | """ 141 | converters = load_converters() 142 | logger = get_logger_wrapper(verbose) 143 | if converter is Converters.OMETIFF: 144 | if not _tiff_exc: 145 | logger.info("Converting to OME-TIFF file") 146 | return converters["tiff_converter"].from_tiledb( 147 | input_path=src, output_path=dest, log=logger, **kwargs 148 | ) 149 | else: 150 | raise _tiff_exc 151 | elif converter is Converters.OMEZARR: 152 | if not _zarr_exc: 153 | logger.info("Converting to OME-Zarr file") 154 | return converters["zarr_converter"].from_tiledb( 155 | input_path=src, output_path=dest, log=logger, **kwargs 156 | ) 157 | else: 158 | raise _zarr_exc 159 | elif converter is Converters.PNG: 160 | logger.info("Converting to PNG file") 161 | return converters["png_converter"].from_tiledb( 162 | input_path=src, output_path=dest, log=logger, **kwargs 163 | ) 164 | else: 165 | raise NotImplementedError( 166 | "Openslide Converter does not support exportation back to bio-imaging formats" 167 | ) 168 | --------------------------------------------------------------------------------