├── .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 |
2 |
3 | [](https://github.com/TileDB-Inc/TileDB-BioImaging/actions/workflows/ci.yml)
4 | 
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 |
2 |
3 | [](https://github.com/TileDB-Inc/TileDB-BioImaging/actions/workflows/ci.yml)
4 | 
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 |
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 |
--------------------------------------------------------------------------------