├── .editorconfig ├── .gitattributes ├── .gitchangelog.rc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build-docs.yml │ ├── build-main.yml │ ├── maintain-stale.yml │ ├── test-and-lint.yml │ └── test-upstreams.yml ├── .gitignore ├── .gitmodules ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── aicsimageio ├── __init__.py ├── aics_image.py ├── constants.py ├── dimensions.py ├── exceptions.py ├── formats.py ├── image_container.py ├── metadata │ ├── README.md │ ├── __init__.py │ └── utils.py ├── py.typed ├── readers │ ├── __init__.py │ ├── array_like_reader.py │ ├── bfio_reader.py │ ├── bioformats_reader.py │ ├── czi_reader.py │ ├── default_reader.py │ ├── dv_reader.py │ ├── lif_reader.py │ ├── nd2_reader.py │ ├── ome_tiff_reader.py │ ├── ome_zarr_reader.py │ ├── reader.py │ ├── sldy_reader │ │ ├── __init__.py │ │ └── sldy_image.py │ ├── tiff_glob_reader.py │ └── tiff_reader.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── image_container_test_utils.py │ ├── metadata │ │ └── __init__.py │ ├── readers │ │ ├── __init__.py │ │ ├── extra_readers │ │ │ ├── __init__.py │ │ │ ├── sldy_reader │ │ │ │ ├── __init__.py │ │ │ │ ├── test_sldy_image.py │ │ │ │ └── test_sldy_reader.py │ │ │ ├── test_bioformats_reader.py │ │ │ ├── test_czi_reader.py │ │ │ ├── test_default_reader.py │ │ │ ├── test_dv_reader.py │ │ │ ├── test_lif_reader.py │ │ │ ├── test_nd2_reader.py │ │ │ ├── test_ome_tiled_tiff_reader.py │ │ │ └── test_ome_zarr_reader.py │ │ ├── test_array_like_reader.py │ │ ├── test_glob_reader.py │ │ ├── test_ome_tiff_reader.py │ │ └── test_tiff_reader.py │ ├── test_aics_image.py │ ├── test_dimensions.py │ ├── test_transforms.py │ ├── utils │ │ ├── __init__.py │ │ └── test_io_utils.py │ └── writers │ │ ├── __init__.py │ │ ├── extra_writers │ │ ├── __init__.py │ │ ├── test_timeseries_writer.py │ │ └── test_two_d_writer.py │ │ ├── test_ome_tiff_writer.py │ │ └── test_ome_zarr_writer.py ├── transforms.py ├── types.py ├── utils │ ├── __init__.py │ └── io_utils.py └── writers │ ├── __init__.py │ ├── ome_tiff_writer.py │ ├── ome_zarr_writer.py │ ├── timeseries_writer.py │ ├── two_d_writer.py │ └── writer.py ├── asv.conf.json ├── benchmarks ├── __init__.py ├── benchmark_chunk_sizes.py ├── benchmark_image_containers.py └── benchmark_lib.py ├── codecov.yml ├── cookiecutter.yaml ├── docs ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── GOVERNANCE.md ├── INSTALLATION.rst ├── MISSION_AND_VALUES.md ├── Makefile ├── ROADMAP.md ├── _fix_internal_links.py ├── conf.py ├── developer_resources.rst ├── index.rst ├── make.bat └── modules.rst ├── presentations └── 2021-dask-life-sciences │ ├── environment.yml │ └── presentation.ipynb ├── scripts ├── TEST_RESOURCES_HASH.txt ├── download_test_resources.py ├── makezarr.ipynb └── upload_test_resources.py ├── setup.cfg ├── setup.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.yml] 14 | indent_size = 2 15 | 16 | [*.bat] 17 | indent_style = tab 18 | end_of_line = crlf 19 | 20 | [LICENSE] 21 | insert_final_newline = false 22 | 23 | [Makefile] 24 | indent_style = tab 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | presentations/** linguist-documentation -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: '"Something''s wrong..."' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## System and Software 11 | * aicsimageio Version: 12 | * Python Version: 13 | * Operating System: 14 | 15 | 16 | ## Description 17 | *A clear description of the bug* 18 | 19 | 20 | 21 | 22 | ## Expected Behavior 23 | *What did you expect to happen instead?* 24 | 25 | 26 | 27 | 28 | ## Reproduction 29 | *A minimal example that exhibits the behavior.* 30 | 31 | 32 | 33 | 34 | ## Environment 35 | *Any additional information about your environment* 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: '"It would be really cool if x did y..."' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Use Case 11 | *Please provide a use case to help us understand your request in context* 12 | 13 | 14 | 15 | 16 | ## Solution 17 | *Please describe your ideal solution* 18 | 19 | 20 | 21 | 22 | ## Alternatives 23 | *Please describe any alternatives you've considered, even if you've dismissed them* 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Pull request recommendations: 5 | - [ ] Name your pull request _your-development-type/short-description_. Ex: _feature/read-tiff-files_ 6 | - [ ] Link to any relevant issue in the PR description. Ex: _Resolves [gh-], adds tiff file format support_ 7 | - [ ] Provide relevant tests for your feature or bug fix. 8 | - [ ] Provide or update documentation for any feature added by your pull request. 9 | 10 | Thanks for contributing! 11 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | docs: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | ref: main 16 | submodules: true 17 | 18 | # v3 Docs 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: 3.9 23 | - name: Checkout v3 Code 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | ref: v3 28 | path: .aicsimageio-v3/ 29 | - name: Generate v3 Docs 30 | run: | 31 | cd .aicsimageio-v3/ 32 | pip install -e .[dev] 33 | gitchangelog 34 | make docs 35 | mkdir -p ../docs/_static/v3/ 36 | cp -r docs/_build/html/* ../docs/_static/v3/ 37 | - name: Cleanup v3 Resources 38 | run: | 39 | rm -Rf .aicsimageio-v3/ 40 | 41 | # Latest Docs 42 | - name: Set up Python 43 | uses: actions/setup-python@v4 44 | with: 45 | python-version: '3.10' 46 | - name: Install Dependencies 47 | run: | 48 | pip install --upgrade pip 49 | pip install .[dev] --no-cache-dir --force-reinstall 50 | - name: Download Test Resources 51 | run: | 52 | python scripts/download_test_resources.py --debug 53 | - name: Get Prior Benchmark Results 54 | uses: actions/checkout@v4 55 | with: 56 | fetch-depth: 0 57 | ref: benchmark-results 58 | path: .asv/ 59 | - name: Run Benchmarks 60 | run: | 61 | asv machine --machine github-ci --yes 62 | asv run --machine github-ci 63 | - name: Store New Benchmark Results 64 | run: | 65 | cd .asv 66 | git config user.name github-actions 67 | git config user.email github-actions@github.com 68 | git add . 69 | git commit -m "Benchmark results for ${{ github.sha }}" 70 | git push 71 | 72 | - name: Generate Latest Docs 73 | run: | 74 | gitchangelog 75 | make gen-docs-full 76 | touch docs/_build/html/.nojekyll 77 | 78 | - name: Publish Docs 79 | uses: JamesIves/github-pages-deploy-action@releases/v3 80 | with: 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | BASE_BRANCH: main # The branch the action should deploy from. 83 | BRANCH: gh-pages # The branch the action should deploy to. 84 | FOLDER: docs/_build/html/ # The folder the action should deploy. 85 | -------------------------------------------------------------------------------- /.github/workflows/build-main.yml: -------------------------------------------------------------------------------- 1 | name: Build Main 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | # 10 | # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07 11 | # Run every Monday at 18:00:00 UTC (Monday at 10:00:00 PST) 12 | - cron: "0 18 * * 1" 13 | 14 | jobs: 15 | test-core-lib: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: [3.9, '3.10', 3.11] 21 | os: [ 22 | ubuntu-latest, 23 | windows-latest, 24 | macos-latest, 25 | ] 26 | steps: 27 | - uses: actions/checkout@v4 28 | with: 29 | submodules: true 30 | - name: Setup Python 31 | uses: actions/setup-python@v4 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | - name: Install Dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install .[test] 38 | - uses: actions/cache@v4 39 | id: cache 40 | with: 41 | path: aicsimageio/tests/resources 42 | key: ${{ hashFiles('scripts/TEST_RESOURCES_HASH.txt') }} 43 | - name: Download Test Resources 44 | if: steps.cache.outputs.cache-hit != 'true' 45 | run: | 46 | python scripts/download_test_resources.py --debug 47 | - name: Run tests with Tox 48 | # Run tox using the version of Python in `PATH` 49 | run: tox -e py 50 | - name: Upload codecov 51 | uses: codecov/codecov-action@v3 52 | 53 | test-readers: 54 | runs-on: ${{ matrix.os }} 55 | strategy: 56 | fail-fast: false 57 | matrix: 58 | python-version: [3.9, '3.10', 3.11] 59 | os: [ 60 | ubuntu-20.04, 61 | windows-latest, 62 | macos-11, 63 | ] 64 | tox-env: [ 65 | bioformats, 66 | czi, 67 | base-imageio, 68 | dv, 69 | lif, 70 | nd2, 71 | sldy, 72 | bfio, 73 | omezarr, 74 | ] 75 | steps: 76 | - uses: actions/checkout@v4 77 | with: 78 | submodules: true 79 | - name: Setup Python 80 | uses: actions/setup-python@v4 81 | with: 82 | python-version: ${{ matrix.python-version }} 83 | - uses: actions/setup-java@v3 84 | with: 85 | distribution: "temurin" 86 | java-version: "11" 87 | - name: Install Dependencies 88 | run: | 89 | python -m pip install --upgrade pip 90 | pip install .[test] 91 | - uses: actions/cache@v4 92 | id: cache 93 | with: 94 | path: aicsimageio/tests/resources 95 | key: ${{ hashFiles('scripts/TEST_RESOURCES_HASH.txt') }} 96 | - name: Download Test Resources 97 | if: steps.cache.outputs.cache-hit != 'true' 98 | run: | 99 | python scripts/download_test_resources.py --debug 100 | - name: Run tests with Tox 101 | # Run tox using the version of Python in `PATH` 102 | run: tox -e ${{ matrix.tox-env }} 103 | - name: Upload codecov 104 | uses: codecov/codecov-action@v2 105 | 106 | lint: 107 | runs-on: ubuntu-latest 108 | steps: 109 | - uses: actions/checkout@v4 110 | with: 111 | submodules: true 112 | - name: Set up Python 113 | uses: actions/setup-python@v4 114 | with: 115 | python-version: 3.11 116 | - name: Install Dependencies 117 | run: | 118 | python -m pip install --upgrade pip 119 | pip install .[test] 120 | - name: Lint 121 | run: tox -e lint 122 | 123 | publish: 124 | if: "contains(github.event.head_commit.message, 'Bump version')" 125 | needs: [test-core-lib, test-readers, lint] 126 | runs-on: ubuntu-latest 127 | # Specifying a GitHub environment is optional, but strongly encouraged 128 | environment: release 129 | permissions: 130 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 131 | 132 | steps: 133 | - uses: actions/checkout@v4 134 | with: 135 | submodules: true 136 | - name: Set up Python 137 | uses: actions/setup-python@v4 138 | with: 139 | python-version: 3.11 140 | - name: Install Dependencies 141 | run: | 142 | python -m pip install --upgrade pip 143 | pip install setuptools wheel 144 | - name: Build Package 145 | run: | 146 | python setup.py sdist bdist_wheel 147 | - name: Publish to PyPI 148 | uses: pypa/gh-action-pypi-publish@release/v1 149 | -------------------------------------------------------------------------------- /.github/workflows/maintain-stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v8 11 | with: 12 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 13 | stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' 14 | days-before-stale: 365 15 | days-before-close: 14 -------------------------------------------------------------------------------- /.github/workflows/test-and-lint.yml: -------------------------------------------------------------------------------- 1 | name: Test and Lint 2 | 3 | on: pull_request 4 | 5 | # Cancel actions when new commits are pushed to PR 6 | concurrency: 7 | group: ${{ github.head_ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | test-core-lib: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [3.9, '3.10', 3.11] 17 | os: [ 18 | ubuntu-latest, 19 | windows-latest, 20 | macos-latest, 21 | ] 22 | steps: 23 | - uses: actions/checkout@v2 24 | with: 25 | submodules: true 26 | - name: Setup Python 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install Dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install .[test] 34 | - uses: actions/cache@v2 35 | id: cache 36 | with: 37 | path: aicsimageio/tests/resources 38 | key: ${{ hashFiles('scripts/TEST_RESOURCES_HASH.txt') }} 39 | - name: Download Test Resources 40 | if: steps.cache.outputs.cache-hit != 'true' 41 | run: | 42 | python scripts/download_test_resources.py --debug 43 | - name: Run tests with Tox 44 | # Run tox using the version of Python in `PATH` 45 | run: tox -e py 46 | - name: Upload codecov 47 | uses: codecov/codecov-action@v2 48 | 49 | test-readers: 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | fail-fast: false 53 | matrix: 54 | python-version: [3.9, '3.10', 3.11] 55 | os: [ 56 | ubuntu-20.04, 57 | windows-latest, 58 | macos-11, 59 | ] 60 | tox-env: [ 61 | bioformats, 62 | czi, 63 | base-imageio, 64 | dv, 65 | lif, 66 | nd2, 67 | sldy, 68 | bfio, 69 | omezarr, 70 | ] 71 | steps: 72 | - uses: actions/checkout@v2 73 | with: 74 | submodules: true 75 | - name: Setup Python 76 | uses: actions/setup-python@v2 77 | with: 78 | python-version: ${{ matrix.python-version }} 79 | - uses: actions/setup-java@v2 80 | with: 81 | distribution: "temurin" 82 | java-version: "11" 83 | - name: Install Dependencies 84 | run: | 85 | python -m pip install --upgrade pip 86 | pip install .[test] 87 | - uses: actions/cache@v2 88 | id: cache 89 | with: 90 | path: aicsimageio/tests/resources 91 | key: ${{ hashFiles('scripts/TEST_RESOURCES_HASH.txt') }} 92 | - name: Download Test Resources 93 | if: steps.cache.outputs.cache-hit != 'true' 94 | run: | 95 | python scripts/download_test_resources.py --debug 96 | - name: Run tests with Tox 97 | run: tox -e ${{ matrix.tox-env }} 98 | - name: Upload codecov 99 | uses: codecov/codecov-action@v2 100 | 101 | lint: 102 | runs-on: ubuntu-latest 103 | steps: 104 | - uses: actions/checkout@v2 105 | with: 106 | submodules: true 107 | - name: Set up Python 108 | uses: actions/setup-python@v1 109 | with: 110 | python-version: 3.11 111 | - name: Install Dependencies 112 | run: | 113 | python -m pip install --upgrade pip 114 | pip install pre-commit 115 | - name: Lint 116 | run: pre-commit run --all-files --show-diff-on-failure 117 | 118 | docs: 119 | runs-on: ubuntu-latest 120 | steps: 121 | - uses: actions/checkout@v4 122 | with: 123 | fetch-depth: 0 124 | submodules: true 125 | - name: Set up Python 126 | uses: actions/setup-python@v4 127 | with: 128 | python-version: 3.11 129 | - name: Install Dependencies 130 | run: | 131 | pip install --upgrade pip 132 | pip install .[dev] 133 | - name: Generate Docs 134 | run: | 135 | gitchangelog 136 | make gen-docs 137 | -------------------------------------------------------------------------------- /.github/workflows/test-upstreams.yml: -------------------------------------------------------------------------------- 1 | # Test the core library against upstream dependencies 2 | # If fail, please report bugs to the appropriate library 3 | 4 | name: Test Upstreams 5 | 6 | on: 7 | workflow_dispatch: 8 | # push: 9 | # branches: 10 | # - main 11 | # schedule: 12 | # # 13 | # # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html#tag_20_25_07 14 | # # Run every Mon,Wed,Thurs at 19:00:00 UTC (Monday at 11:00:00 PST) 15 | # - cron: "0 19 * * 1,3,4" 16 | 17 | jobs: 18 | test-core-lib: 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | python-version: [3.9, '3.10', 3.11] 24 | os: [ 25 | ubuntu-latest, 26 | windows-latest, 27 | macos-latest, 28 | ] 29 | steps: 30 | - uses: actions/checkout@v4 31 | with: 32 | submodules: true 33 | - name: Setup Python 34 | uses: actions/setup-python@v4 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | - name: Configure AWS Credentials 38 | uses: aws-actions/configure-aws-credentials@v3 39 | with: 40 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 41 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 42 | aws-region: us-west-2 43 | - name: Install Dependencies 44 | run: | 45 | python -m pip install --upgrade pip 46 | pip install .[test] 47 | - uses: actions/cache@v4 48 | id: cache 49 | with: 50 | path: aicsimageio/tests/resources 51 | key: ${{ hashFiles('scripts/TEST_RESOURCES_HASH.txt') }} 52 | - name: Download Test Resources 53 | if: steps.cache.outputs.cache-hit != 'true' 54 | run: | 55 | python scripts/download_test_resources.py --debug 56 | - name: Run tests with Tox 57 | run: tox -e upstreams -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # OS generated files 29 | .DS_Store 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | docs/aicsimageio.*rst 71 | 72 | # Dask workers 73 | dask-worker-space/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # dotenv 91 | .env 92 | 93 | # virtualenv 94 | .venv 95 | venv/ 96 | ENV/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Pycharm project stuff 103 | .idea 104 | # Rope project settings 105 | .ropeproject 106 | # VSCode 107 | .vscode 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | 115 | # Project specific standalone files 116 | workbench.ipynb 117 | 118 | # Ignore test resources directory 119 | resources/ 120 | writer_products/ 121 | 122 | # asv / benchmarks 123 | .asv/html 124 | .asv/env 125 | .asv/results 126 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "aicsimageio/metadata/czi-to-ome-xslt"] 2 | path = aicsimageio/metadata/czi-to-ome-xslt 3 | url = https://github.com/AllenCellModeling/czi-to-ome-xslt.git 4 | branch = main 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: aicsimageio/metadata/czi-to-ome-xslt/ 2 | files: aicsimageio 3 | repos: 4 | - repo: https://github.com/PyCQA/flake8 5 | rev: 3.9.2 6 | hooks: 7 | - id: flake8 8 | additional_dependencies: [flake8-typing-imports, flake8-debugger] 9 | args: 10 | [ 11 | --count, 12 | --show-source, 13 | --statistics, 14 | --min-python-version=3.9.0, 15 | ] 16 | - repo: https://github.com/myint/autoflake 17 | rev: v1.4 18 | hooks: 19 | - id: autoflake 20 | args: ["--in-place", "--remove-all-unused-imports"] 21 | - repo: https://github.com/PyCQA/isort 22 | rev: 5.11.5 23 | hooks: 24 | - id: isort 25 | - repo: https://github.com/psf/black 26 | rev: 22.3.0 27 | hooks: 28 | - id: black 29 | - repo: https://github.com/pre-commit/mirrors-mypy 30 | rev: v0.910 31 | hooks: 32 | - id: mypy 33 | additional_dependencies: [types-PyYAML==6.0.12.9] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | Copyright (c) 2022, Allen Institute for Cell Science 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, this 13 | list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from this 18 | software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 23 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 24 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 25 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 27 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 28 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 29 | OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | include aicsimageio/py.typed 4 | 5 | recursive-include tests * 6 | recursive-exclude * __pycache__ 7 | recursive-exclude * *.py[co] 8 | 9 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 10 | recursive-include aicsimageio/metadata *.xsl 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean build gen-docs gen-docs-full docs prepare-release update-from-cookiecutter help 2 | .DEFAULT_GOAL := help 3 | 4 | define BROWSER_PYSCRIPT 5 | import os, webbrowser, sys 6 | 7 | try: 8 | from urllib import pathname2url 9 | except: 10 | from urllib.request import pathname2url 11 | 12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 13 | endef 14 | export BROWSER_PYSCRIPT 15 | 16 | define PRINT_HELP_PYSCRIPT 17 | import re, sys 18 | 19 | for line in sys.stdin: 20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) 21 | if match: 22 | target, help = match.groups() 23 | print("%-20s %s" % (target, help)) 24 | endef 25 | export PRINT_HELP_PYSCRIPT 26 | 27 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 28 | 29 | help: 30 | @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) 31 | 32 | clean: ## clean all build, python, and testing files 33 | rm -fr build/ 34 | rm -fr dist/ 35 | rm -fr .eggs/ 36 | find . -name '*.egg-info' -exec rm -fr {} + 37 | find . -name '*.egg' -exec rm -fr {} + 38 | find . -name '*.pyc' -exec rm -fr {} + 39 | find . -name '*.pyo' -exec rm -fr {} + 40 | find . -name '*~' -exec rm -fr {} + 41 | find . -name '__pycache__' -exec rm -fr {} + 42 | rm -fr .tox/ 43 | rm -fr .coverage 44 | rm -fr coverage.xml 45 | rm -fr htmlcov/ 46 | rm -fr .pytest_cache 47 | 48 | build: ## run tox / run tests and lint for local IO only 49 | tox -- -k "not REMOTE" 50 | 51 | build-with-remote: ## run full tox / test and lint suite, including remote IO 52 | tox 53 | 54 | gen-docs: ## generate Sphinx HTML documentation, including API docs 55 | rm -f docs/aicsimageio*.rst 56 | rm -f docs/modules.rst 57 | sphinx-apidoc -o docs/ aicsimageio **/tests/ 58 | sphinx-autogen docs/index.rst 59 | python docs/_fix_internal_links.py --current-suffix .md --target-suffix .html 60 | $(MAKE) -C docs html 61 | python docs/_fix_internal_links.py --current-suffix .html --target-suffix .md 62 | 63 | gen-docs-full: ## generate Sphinx docs + benchmark docs 64 | make gen-docs 65 | asv publish 66 | cp -r .asv/html/ docs/_build/html/_benchmarks 67 | 68 | docs: ## generate Sphinx HTML documentation, including API docs, and serve to browser 69 | make gen-docs 70 | $(BROWSER) docs/_build/html/index.html 71 | 72 | update-from-cookiecutter: ## update this repo using latest cookiecutter-pypackage 73 | cookiecutter gh:AllenCellModeling/cookiecutter-pypackage --config-file cookiecutter.yaml --no-input --overwrite-if-exists --output-dir .. -------------------------------------------------------------------------------- /aicsimageio/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """Top-level package for AICSImageIO.""" 5 | 6 | from .aics_image import AICSImage # noqa: F401 7 | from .aics_image import imread # noqa: F401 8 | from .aics_image import imread_dask # noqa: F401 9 | from .aics_image import imread_xarray # noqa: F401 10 | from .aics_image import imread_xarray_dask # noqa: F401 11 | 12 | __author__ = "Eva Maxfield Brown, Allen Institute for Cell Science" 13 | __email__ = "evamaxfieldbrown@gmail.com, jamie.sherman@gmail.com, bowdenm@spu.edu" 14 | # Do not edit this string manually, always use bumpversion 15 | # Details in CONTRIBUTING.md 16 | __version__ = "4.14.0" 17 | 18 | 19 | def get_module_version() -> str: 20 | return __version__ 21 | -------------------------------------------------------------------------------- /aicsimageio/constants.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | ############################################################################### 5 | 6 | METADATA_UNPROCESSED = "unprocessed" 7 | METADATA_PROCESSED = "processed" 8 | -------------------------------------------------------------------------------- /aicsimageio/dimensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from collections.abc import Sequence as seq 5 | from typing import Collection, ItemsView, Sequence, Tuple, Union 6 | 7 | ############################################################################### 8 | 9 | 10 | class DimensionNames: 11 | Time = "T" 12 | Channel = "C" 13 | SpatialZ = "Z" 14 | SpatialY = "Y" 15 | SpatialX = "X" 16 | Samples = "S" 17 | MosaicTile = "M" 18 | 19 | 20 | DEFAULT_DIMENSION_ORDER_LIST = [ 21 | DimensionNames.Time, 22 | DimensionNames.Channel, 23 | DimensionNames.SpatialZ, 24 | DimensionNames.SpatialY, 25 | DimensionNames.SpatialX, 26 | ] 27 | DEFAULT_DIMENSION_ORDER_LIST_WITH_SAMPLES = DEFAULT_DIMENSION_ORDER_LIST + [ 28 | DimensionNames.Samples 29 | ] 30 | DEFAULT_DIMENSION_ORDER_LIST_WITH_MOSAIC_TILES = [ 31 | DimensionNames.MosaicTile 32 | ] + DEFAULT_DIMENSION_ORDER_LIST 33 | DEFAULT_DIMENSION_ORDER_LIST_WITH_MOSAIC_TILES_AND_SAMPLES = ( 34 | [DimensionNames.MosaicTile] 35 | + DEFAULT_DIMENSION_ORDER_LIST 36 | + [DimensionNames.Samples] 37 | ) 38 | 39 | DEFAULT_DIMENSION_ORDER = "".join(DEFAULT_DIMENSION_ORDER_LIST) 40 | DEFAULT_DIMENSION_ORDER_WITH_SAMPLES = "".join( 41 | DEFAULT_DIMENSION_ORDER_LIST_WITH_SAMPLES 42 | ) 43 | DEFAULT_DIMENSION_ORDER_WITH_MOSAIC_TILES = "".join( 44 | DEFAULT_DIMENSION_ORDER_LIST_WITH_MOSAIC_TILES 45 | ) 46 | DEFAULT_DIMENSION_ORDER_WITH_MOSAIC_TILES_AND_SAMPLES = "".join( 47 | DEFAULT_DIMENSION_ORDER_LIST_WITH_MOSAIC_TILES_AND_SAMPLES 48 | ) 49 | 50 | DEFAULT_CHUNK_DIMS = [ 51 | DimensionNames.SpatialZ, 52 | DimensionNames.SpatialY, 53 | DimensionNames.SpatialX, 54 | DimensionNames.Samples, 55 | ] 56 | 57 | REQUIRED_CHUNK_DIMS = [ 58 | DimensionNames.SpatialY, 59 | DimensionNames.SpatialX, 60 | DimensionNames.Samples, 61 | ] 62 | 63 | ############################################################################### 64 | 65 | 66 | class Dimensions: 67 | def __init__(self, dims: Collection[str], shape: Tuple[int, ...]): 68 | """ 69 | A general object for managing the pairing of dimension name and dimension size. 70 | 71 | Parameters 72 | ---------- 73 | dims: Collection[str] 74 | An ordered string or collection of the dimensions to pair with their sizes. 75 | shape: Tuple[int, ...] 76 | An ordered tuple of the dimensions sizes to pair with their names. 77 | 78 | Examples 79 | -------- 80 | >>> dims = Dimensions("TCZYX", (1, 4, 75, 624, 924)) 81 | ... dims.X 82 | ... dims['T', 'X'] 83 | """ 84 | # zip(strict=True) only in Python 3.10 85 | # Check equal length dims and shape 86 | if len(dims) != len(shape): 87 | raise ValueError( 88 | f"Number of dimensions provided ({len(dims)} -- '{dims}') " 89 | f"does not match shape size provided ({len(shape)} -- '{shape}')." 90 | ) 91 | 92 | # Make dims a string 93 | if not isinstance(dims, str): 94 | if any([len(c) != 1 for c in dims]): 95 | raise ValueError( 96 | f"When providing a list of dimension strings, " 97 | f"each dimension may only be a single character long " 98 | f"(received: '{dims}')." 99 | ) 100 | dims = "".join(dims) 101 | 102 | # Store order and shape 103 | self._order = dims 104 | self._shape = shape 105 | 106 | # Create attributes 107 | self._dims_shape = dict(zip(dims, shape)) 108 | for dim, size in self._dims_shape.items(): 109 | setattr(self, dim, size) 110 | 111 | @property 112 | def order(self) -> str: 113 | """ 114 | Returns 115 | ------- 116 | order: str 117 | The natural order of the dimensions as a single string. 118 | """ 119 | return self._order 120 | 121 | @property 122 | def shape(self) -> Tuple[int, ...]: 123 | """ 124 | Returns 125 | ------- 126 | shape: Tuple[int, ...] 127 | The dimension sizes in their natural order. 128 | """ 129 | return self._shape 130 | 131 | def items(self) -> ItemsView[str, int]: 132 | return self._dims_shape.items() 133 | 134 | def __str__(self) -> str: 135 | dims_string = ", ".join([f"{dim}: {size}" for dim, size in self.items()]) 136 | return f"" 137 | 138 | def __repr__(self) -> str: 139 | return str(self) 140 | 141 | def __getitem__(self, key: Union[str, Sequence[str]]) -> Tuple[int, ...]: 142 | if isinstance(key, str): 143 | if key not in self._order: 144 | raise IndexError(f"{key} not in {self._order}") 145 | return (self._dims_shape[key],) 146 | elif isinstance(key, seq) and all(isinstance(k, str) for k in key): 147 | invalid_dims = [] 148 | for k in key: 149 | if k not in self._order: 150 | invalid_dims.append(k) 151 | if len(invalid_dims) == 0: 152 | return tuple(self._dims_shape[k] for k in key) 153 | else: 154 | raise IndexError(f"{', '.join(invalid_dims)} not in {self._order}") 155 | else: 156 | raise TypeError( 157 | f"Key must be a string or list of strings but got type {type(key)}" 158 | ) 159 | 160 | def __setattr__(self, __name: str, __value: int) -> None: 161 | super().__setattr__(__name, __value) 162 | 163 | def __getattr__(self, __name: str) -> int: 164 | # TODO: Py310 __match_args__ for better typing 165 | return super().__getattribute__(__name) 166 | -------------------------------------------------------------------------------- /aicsimageio/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Optional 5 | 6 | 7 | class ConflictingArgumentsError(Exception): 8 | """ 9 | This exception is returned when 2 arguments to the same function are in conflict. 10 | """ 11 | 12 | 13 | class InvalidDimensionOrderingError(Exception): 14 | """ 15 | A general exception that can be thrown when handling dimension ordering or 16 | validation. Should be provided with a message for the user to be given more context. 17 | """ 18 | 19 | 20 | class UnexpectedShapeError(Exception): 21 | """ 22 | A general exception that can be thrown when handling shape validation. 23 | Should be provided with a message for the user to be given more context. 24 | """ 25 | 26 | 27 | class UnsupportedFileFormatError(Exception): 28 | """ 29 | This exception is intended to communicate that the file extension is not one of 30 | the supported file types and cannot be parsed with AICSImage. 31 | """ 32 | 33 | def __init__(self, reader_name: str, path: str, msg_extra: Optional[str] = None): 34 | super().__init__() 35 | self.reader_name = reader_name 36 | self.path = path 37 | self.msg_extra = msg_extra 38 | 39 | def __str__(self) -> str: 40 | msg = f"{self.reader_name} does not support the image: '{self.path}'." 41 | 42 | if self.msg_extra is not None: 43 | msg = f"{msg} {self.msg_extra}" 44 | 45 | return msg 46 | -------------------------------------------------------------------------------- /aicsimageio/image_container.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from abc import ABC, abstractmethod 5 | from typing import Any, List, Optional, Tuple, Union 6 | 7 | import dask.array as da 8 | import numpy as np 9 | import xarray as xr 10 | 11 | from .dimensions import Dimensions 12 | from .types import PhysicalPixelSizes 13 | 14 | ############################################################################### 15 | 16 | 17 | class ImageContainer(ABC): 18 | @property 19 | @abstractmethod 20 | def scenes(self) -> Tuple[str, ...]: 21 | pass 22 | 23 | @property 24 | def current_scene(self) -> str: 25 | pass 26 | 27 | @property 28 | def current_scene_index(self) -> int: 29 | pass 30 | 31 | @abstractmethod 32 | def set_scene(self, scene_id: Union[str, int]) -> None: 33 | pass 34 | 35 | @property 36 | def xarray_dask_data(self) -> xr.DataArray: 37 | pass 38 | 39 | @property 40 | def xarray_data(self) -> xr.DataArray: 41 | pass 42 | 43 | @property 44 | def dask_data(self) -> da.Array: 45 | pass 46 | 47 | @property 48 | def data(self) -> np.ndarray: 49 | pass 50 | 51 | @property 52 | def dtype(self) -> np.dtype: 53 | pass 54 | 55 | @property 56 | def shape(self) -> Tuple[int, ...]: 57 | pass 58 | 59 | @property 60 | def dims(self) -> Dimensions: 61 | pass 62 | 63 | @abstractmethod 64 | def get_image_dask_data( 65 | self, dimension_order_out: Optional[str] = None, **kwargs: Any 66 | ) -> da.Array: 67 | pass 68 | 69 | @abstractmethod 70 | def get_image_data( 71 | self, dimension_order_out: Optional[str] = None, **kwargs: Any 72 | ) -> np.ndarray: 73 | pass 74 | 75 | @property 76 | def metadata(self) -> Any: 77 | pass 78 | 79 | @property 80 | def channel_names(self) -> Optional[List[str]]: 81 | pass 82 | 83 | @property 84 | def physical_pixel_sizes(self) -> PhysicalPixelSizes: 85 | pass 86 | -------------------------------------------------------------------------------- /aicsimageio/metadata/README.md: -------------------------------------------------------------------------------- 1 | # AICSImage Metadata Module 2 | 3 | This submodule contains metadata utility functions, as well as a Git 4 | submodule which contains the CZI to OME XSLT used by the CZI reader. 5 | 6 | -------------------------------------------------------------------------------- /aicsimageio/metadata/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenCellModeling/aicsimageio/5f20fbd3a3402d1a9758b812cb648fb41e2ab0b3/aicsimageio/py.typed -------------------------------------------------------------------------------- /aicsimageio/readers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import TYPE_CHECKING, Type 5 | 6 | # To add a new reader add it both to TYPE_CHECKING and _READERS 7 | 8 | if TYPE_CHECKING: 9 | from .array_like_reader import ArrayLikeReader # noqa: F401 10 | from .bfio_reader import OmeTiledTiffReader # noqa: F401 11 | from .bioformats_reader import BioformatsReader # noqa: F401 12 | from .czi_reader import CziReader # noqa: F401 13 | from .dv_reader import DVReader # noqa: F401 14 | from .lif_reader import LifReader # noqa: F401 15 | from .nd2_reader import ND2Reader # noqa: F401 16 | from .ome_tiff_reader import OmeTiffReader # noqa: F401 17 | from .ome_zarr_reader import OmeZarrReader # noqa: F401 18 | from .reader import Reader 19 | from .sldy_reader import SldyReader # noqa: F401 20 | from .tiff_glob_reader import TiffGlobReader # noqa: F401 21 | from .tiff_reader import TiffReader # noqa: F401 22 | 23 | 24 | # add ".relativepath.ClassName" 25 | _READERS = ( 26 | ".array_like_reader.ArrayLikeReader", 27 | ".bfio_reader.OmeTiledTiffReader", 28 | ".bioformats_reader.BioformatsReader", 29 | ".czi_reader.CziReader", 30 | ".dv_reader.DVReader", 31 | ".lif_reader.LifReader", 32 | ".nd2_reader.ND2Reader", 33 | ".ome_tiff_reader.OmeTiffReader", 34 | ".sldy_reader.SldyReader", 35 | ".tiff_reader.TiffReader", 36 | ".tiff_glob_reader.TiffGlobReader", 37 | ".ome_zarr_reader.OmeZarrReader", 38 | ) 39 | _LOOKUP = {k.rsplit(".", 1)[-1]: k for k in _READERS} 40 | __all__ = list(_LOOKUP) 41 | 42 | 43 | def __getattr__(name: str) -> Type["Reader"]: 44 | if name in _LOOKUP: 45 | from importlib import import_module 46 | 47 | path, clsname = _LOOKUP[name].rsplit(".", 1) 48 | mod = import_module(path, __name__) 49 | return getattr(mod, clsname) 50 | raise AttributeError(f"module {__name__!r} has no attribute import name {name!r}") 51 | -------------------------------------------------------------------------------- /aicsimageio/readers/dv_reader.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, Dict, Tuple 4 | 5 | from fsspec.implementations.local import LocalFileSystem 6 | from resource_backed_dask_array import resource_backed_dask_array 7 | 8 | from .. import constants, exceptions, types 9 | from ..utils import io_utils 10 | from .reader import Reader 11 | 12 | if TYPE_CHECKING: 13 | import xarray as xr 14 | from fsspec.spec import AbstractFileSystem 15 | 16 | 17 | try: 18 | from mrc import DVFile 19 | except ImportError: 20 | raise ImportError( 21 | "The mrc package is required for this reader. " 22 | "Install with `pip install aicsimageio[dv]`" 23 | ) 24 | 25 | 26 | class DVReader(Reader): 27 | """Read DV/Deltavision files. 28 | 29 | This reader requires `mrc` to be installed in the environment. 30 | 31 | Parameters 32 | ---------- 33 | image : Path or str 34 | path to file 35 | fs_kwargs: Dict[str, Any] 36 | Any specific keyword arguments to pass down to the fsspec created filesystem. 37 | Default: {} 38 | 39 | Raises 40 | ------ 41 | exceptions.UnsupportedFileFormatError 42 | If the file is not a supported dv file. 43 | """ 44 | 45 | @staticmethod 46 | def _is_supported_image(fs: AbstractFileSystem, path: str, **kwargs: Any) -> bool: 47 | return DVFile.is_supported_file(path) 48 | 49 | def __init__(self, image: types.PathLike, fs_kwargs: Dict[str, Any] = {}): 50 | self._fs, self._path = io_utils.pathlike_to_fs( 51 | image, 52 | enforce_exists=True, 53 | fs_kwargs=fs_kwargs, 54 | ) 55 | # Catch non-local file system 56 | if not isinstance(self._fs, LocalFileSystem): 57 | raise NotImplementedError( 58 | f"dv reader not yet implemented for non-local file system. " 59 | f"Received URI: {self._path}, which points to {type(self._fs)}." 60 | ) 61 | 62 | if not self._is_supported_image(self._fs, self._path): 63 | raise exceptions.UnsupportedFileFormatError( 64 | self.__class__.__name__, self._path 65 | ) 66 | 67 | @property 68 | def scenes(self) -> Tuple[str, ...]: 69 | return ("Image:0",) 70 | 71 | def _read_delayed(self) -> xr.DataArray: 72 | return self._xarr_reformat(delayed=True) 73 | 74 | def _read_immediate(self) -> xr.DataArray: 75 | return self._xarr_reformat(delayed=False) 76 | 77 | def _xarr_reformat(self, delayed: bool) -> xr.DataArray: 78 | with DVFile(self._path) as dv: 79 | xarr = dv.to_xarray(delayed=delayed, squeeze=False) 80 | if delayed: 81 | xarr.data = resource_backed_dask_array(xarr.data, dv) 82 | xarr.attrs[constants.METADATA_UNPROCESSED] = xarr.attrs.pop("metadata") 83 | return xarr 84 | 85 | @property 86 | def physical_pixel_sizes(self) -> types.PhysicalPixelSizes: 87 | """ 88 | Returns 89 | ------- 90 | sizes: PhysicalPixelSizes 91 | Using available metadata, the floats representing physical pixel sizes for 92 | dimensions Z, Y, and X. 93 | 94 | Notes 95 | ----- 96 | We currently do not handle unit attachment to these values. Please see the file 97 | metadata for unit information. 98 | """ 99 | with DVFile(self._path) as dvfile: 100 | return types.PhysicalPixelSizes(*dvfile.voxel_size[::-1]) 101 | -------------------------------------------------------------------------------- /aicsimageio/readers/nd2_reader.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Any, Dict, Tuple 4 | 5 | from fsspec.implementations.local import LocalFileSystem 6 | 7 | from .. import constants, exceptions, types 8 | from ..utils import io_utils 9 | from .reader import Reader 10 | 11 | if TYPE_CHECKING: 12 | import xarray as xr 13 | from fsspec.spec import AbstractFileSystem 14 | from ome_types import OME 15 | 16 | 17 | try: 18 | import nd2 19 | except ImportError: 20 | raise ImportError( 21 | "The nd2 package is required for this reader. " 22 | "Install with `pip install aicsimageio[nd2]`" 23 | ) 24 | 25 | 26 | class ND2Reader(Reader): 27 | """Read NIS-Elements files using the Nikon nd2 SDK. 28 | 29 | This reader requires `nd2` to be installed in the environment. 30 | 31 | Parameters 32 | ---------- 33 | image : Path or str 34 | path to file 35 | fs_kwargs: Dict[str, Any] 36 | Any specific keyword arguments to pass down to the fsspec created filesystem. 37 | Default: {} 38 | 39 | Raises 40 | ------ 41 | exceptions.UnsupportedFileFormatError 42 | If the file is not supported by ND2. 43 | """ 44 | 45 | @staticmethod 46 | def _is_supported_image(fs: AbstractFileSystem, path: str, **kwargs: Any) -> bool: 47 | return nd2.is_supported_file(path, fs.open) 48 | 49 | def __init__(self, image: types.PathLike, fs_kwargs: Dict[str, Any] = {}): 50 | self._fs, self._path = io_utils.pathlike_to_fs( 51 | image, 52 | enforce_exists=True, 53 | fs_kwargs=fs_kwargs, 54 | ) 55 | # Catch non-local file system 56 | if not isinstance(self._fs, LocalFileSystem): 57 | raise ValueError( 58 | f"Cannot read ND2 from non-local file system. " 59 | f"Received URI: {self._path}, which points to {type(self._fs)}." 60 | ) 61 | 62 | if not self._is_supported_image(self._fs, self._path): 63 | raise exceptions.UnsupportedFileFormatError( 64 | self.__class__.__name__, self._path 65 | ) 66 | 67 | @property 68 | def scenes(self) -> Tuple[str, ...]: 69 | with nd2.ND2File(self._path) as rdr: 70 | return tuple(rdr._position_names()) 71 | 72 | def _read_delayed(self) -> xr.DataArray: 73 | return self._xarr_reformat(delayed=True) 74 | 75 | def _read_immediate(self) -> xr.DataArray: 76 | return self._xarr_reformat(delayed=False) 77 | 78 | def _xarr_reformat(self, delayed: bool) -> xr.DataArray: 79 | with nd2.ND2File(self._path) as rdr: 80 | xarr = rdr.to_xarray( 81 | delayed=delayed, squeeze=False, position=self.current_scene_index 82 | ) 83 | xarr.attrs[constants.METADATA_UNPROCESSED] = xarr.attrs.pop("metadata") 84 | if self.current_scene_index is not None: 85 | xarr.attrs[constants.METADATA_UNPROCESSED][ 86 | "frame" 87 | ] = rdr.frame_metadata(self.current_scene_index) 88 | 89 | # include OME metadata as attrs of returned xarray.DataArray if possible 90 | # (not possible with `nd2` version < 0.7.0; see PR #521) 91 | try: 92 | xarr.attrs[constants.METADATA_PROCESSED] = self.ome_metadata 93 | except NotImplementedError: 94 | pass 95 | 96 | return xarr.isel({nd2.AXIS.POSITION: 0}, missing_dims="ignore") 97 | 98 | @property 99 | def physical_pixel_sizes(self) -> types.PhysicalPixelSizes: 100 | """ 101 | Returns 102 | ------- 103 | sizes: PhysicalPixelSizes 104 | Using available metadata, the floats representing physical pixel sizes for 105 | dimensions Z, Y, and X. 106 | 107 | Notes 108 | ----- 109 | We currently do not handle unit attachment to these values. Please see the file 110 | metadata for unit information. 111 | """ 112 | with nd2.ND2File(self._path) as rdr: 113 | return types.PhysicalPixelSizes(*rdr.voxel_size()[::-1]) 114 | 115 | @property 116 | def ome_metadata(self) -> OME: 117 | """Return OME metadata. 118 | 119 | Returns 120 | ------- 121 | metadata: OME 122 | The original metadata transformed into the OME specfication. 123 | This likely isn't a complete transformation but is guarenteed to 124 | be a valid transformation. 125 | 126 | Raises 127 | ------ 128 | NotImplementedError 129 | No metadata transformer available. 130 | """ 131 | if hasattr(nd2.ND2File, "ome_metadata"): 132 | with nd2.ND2File(self._path) as rdr: 133 | return rdr.ome_metadata() 134 | raise NotImplementedError() 135 | -------------------------------------------------------------------------------- /aicsimageio/readers/ome_zarr_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import warnings 4 | from typing import Any, Dict, List, Optional, Tuple 5 | 6 | import xarray as xr 7 | from fsspec.spec import AbstractFileSystem 8 | 9 | from .. import constants, exceptions, types 10 | from ..dimensions import DimensionNames 11 | from ..metadata import utils as metadata_utils 12 | from ..types import PhysicalPixelSizes 13 | from ..utils import io_utils 14 | from .reader import Reader 15 | 16 | try: 17 | from ome_zarr.io import parse_url 18 | from ome_zarr.reader import Reader as ZarrReader 19 | 20 | except ImportError: 21 | raise ImportError( 22 | "ome_zarr is required for this reader. " "Install with `pip install 'ome_zarr'`" 23 | ) 24 | 25 | ############################################################################### 26 | 27 | 28 | class OmeZarrReader(Reader): 29 | """ 30 | Wraps the ome-zarr-py API to provide the same aicsimageio Reader API but for 31 | OmeZarr images. 32 | 33 | Parameters 34 | ---------- 35 | image: types.PathLike 36 | Path to image file to construct Reader for. 37 | fs_kwargs: Dict[str, Any] 38 | Any specific keyword arguments to pass down to the fsspec created filesystem. 39 | Default: {} 40 | """ 41 | 42 | @staticmethod 43 | def _is_supported_image(fs: AbstractFileSystem, path: str, **kwargs: Any) -> bool: 44 | try: 45 | ZarrReader(parse_url(path, mode="r")) 46 | return True 47 | 48 | except AttributeError: 49 | return False 50 | 51 | def __init__( 52 | self, 53 | image: types.PathLike, 54 | fs_kwargs: Dict[str, Any] = {}, 55 | ): 56 | # Expand details of provided image 57 | self._fs, self._path = io_utils.pathlike_to_fs( 58 | image, 59 | enforce_exists=False, 60 | fs_kwargs=fs_kwargs, 61 | ) 62 | 63 | # Enforce valid image 64 | if not self._is_supported_image(self._fs, self._path): 65 | raise exceptions.UnsupportedFileFormatError( 66 | self.__class__.__name__, self._path 67 | ) 68 | 69 | self._zarr = ZarrReader(parse_url(self._path, mode="r")).zarr 70 | self._physical_pixel_sizes: Optional[PhysicalPixelSizes] = None 71 | self._multiresolution_level = 0 72 | self._channel_names: Optional[List[str]] = None 73 | 74 | @property 75 | def scenes(self) -> Tuple[str, ...]: 76 | if self._scenes is None: 77 | scenes = self._zarr.root_attrs["multiscales"] 78 | 79 | # if (each scene has a name) and (that name is unique) use name. 80 | # otherwise generate scene names. 81 | if all("name" in scene for scene in scenes) and ( 82 | len({scene["name"] for scene in scenes}) == len(scenes) 83 | ): 84 | self._scenes = tuple(str(scene["name"]) for scene in scenes) 85 | else: 86 | self._scenes = tuple( 87 | metadata_utils.generate_ome_image_id(i) 88 | for i in range(len(self._zarr.root_attrs["multiscales"])) 89 | ) 90 | return self._scenes 91 | 92 | @property 93 | def physical_pixel_sizes(self) -> PhysicalPixelSizes: 94 | """Return the physical pixel sizes of the image.""" 95 | if self._physical_pixel_sizes is None: 96 | try: 97 | z_size, y_size, x_size = OmeZarrReader._get_pixel_size( 98 | self._zarr, 99 | list(self.dims.order), 100 | self._current_scene_index, 101 | self._multiresolution_level, 102 | ) 103 | except Exception as e: 104 | warnings.warn(f"Could not parse zarr pixel size: {e}") 105 | z_size, y_size, x_size = None, None, None 106 | 107 | self._physical_pixel_sizes = PhysicalPixelSizes(z_size, y_size, x_size) 108 | return self._physical_pixel_sizes 109 | 110 | @property 111 | def channel_names(self) -> Optional[List[str]]: 112 | if self._channel_names is None: 113 | try: 114 | self._channel_names = [ 115 | str(channel["label"]) 116 | for channel in self._zarr.root_attrs["omero"]["channels"] 117 | ] 118 | except KeyError: 119 | self._channel_names = super().channel_names 120 | return self._channel_names 121 | 122 | def _read_delayed(self) -> xr.DataArray: 123 | return self._xarr_format(delayed=True) 124 | 125 | def _read_immediate(self) -> xr.DataArray: 126 | return self._xarr_format(delayed=False) 127 | 128 | def _xarr_format(self, delayed: bool) -> xr.DataArray: 129 | image_data = self._zarr.load(str(self.current_scene_index)) 130 | 131 | axes = self._zarr.root_attrs["multiscales"][self.current_scene_index].get( 132 | "axes" 133 | ) 134 | if axes: 135 | dims = [sub["name"].upper() for sub in axes] 136 | else: 137 | dims = list(OmeZarrReader._guess_dim_order(image_data.shape)) 138 | 139 | if not delayed: 140 | image_data = image_data.compute() 141 | 142 | coords = self._get_coords( 143 | dims, 144 | image_data.shape, 145 | scene=self.current_scene, 146 | channel_names=self.channel_names, 147 | ) 148 | 149 | return xr.DataArray( 150 | image_data, 151 | dims=dims, 152 | coords=coords, 153 | attrs={constants.METADATA_UNPROCESSED: self._zarr.root_attrs}, 154 | ) 155 | 156 | @staticmethod 157 | def _get_coords( 158 | dims: List[str], 159 | shape: Tuple[int, ...], 160 | scene: str, 161 | channel_names: Optional[List[str]], 162 | ) -> Dict[str, Any]: 163 | 164 | coords: Dict[str, Any] = {} 165 | 166 | # Use dims for coord determination 167 | if DimensionNames.Channel in dims: 168 | # Generate channel names if no existing channel names 169 | if channel_names is None: 170 | coords[DimensionNames.Channel] = [ 171 | metadata_utils.generate_ome_channel_id(image_id=scene, channel_id=i) 172 | for i in range(shape[dims.index(DimensionNames.Channel)]) 173 | ] 174 | else: 175 | coords[DimensionNames.Channel] = channel_names 176 | 177 | return coords 178 | 179 | @staticmethod 180 | def _get_pixel_size( 181 | reader: ZarrReader, dims: List[str], series_index: int, resolution_index: int 182 | ) -> Tuple[Optional[float], Optional[float], Optional[float]]: 183 | 184 | # OmeZarr file may contain an additional set of "coordinateTransformations" 185 | # these coefficents are applied to all resolution levels. 186 | if ( 187 | "coordinateTransformations" 188 | in reader.root_attrs["multiscales"][series_index] 189 | ): 190 | universal_res_consts = reader.root_attrs["multiscales"][series_index][ 191 | "coordinateTransformations" 192 | ][0]["scale"] 193 | else: 194 | universal_res_consts = [1.0 for _ in range(len(dims))] 195 | 196 | coord_transform = reader.root_attrs["multiscales"][series_index]["datasets"][ 197 | resolution_index 198 | ]["coordinateTransformations"] 199 | 200 | spatial_coeffs = {} 201 | 202 | for dim in [ 203 | DimensionNames.SpatialX, 204 | DimensionNames.SpatialY, 205 | DimensionNames.SpatialZ, 206 | ]: 207 | if dim in dims: 208 | dim_index = dims.index(dim) 209 | spatial_coeffs[dim] = ( 210 | coord_transform[0]["scale"][dim_index] 211 | * universal_res_consts[dim_index] 212 | ) 213 | else: 214 | spatial_coeffs[dim] = None 215 | 216 | return ( 217 | spatial_coeffs[DimensionNames.SpatialZ], 218 | spatial_coeffs[DimensionNames.SpatialY], 219 | spatial_coeffs[DimensionNames.SpatialX], 220 | ) 221 | -------------------------------------------------------------------------------- /aicsimageio/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Unit test package for aicsimageio.""" 4 | -------------------------------------------------------------------------------- /aicsimageio/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from pathlib import Path 5 | from typing import Any, Tuple, Union 6 | 7 | import dask.array as da 8 | import numpy as np 9 | import pytest 10 | 11 | ############################################################################### 12 | 13 | 14 | LOCAL = "LOCAL" 15 | REMOTE = "REMOTE" 16 | 17 | LOCAL_RESOURCES_DIR = Path(__file__).parent / "resources" 18 | REMOTE_RESOURCES_DIR = "s3://aics-modeling-packages-test-resources/aicsimageio/test_resources/resources" # noqa: E501 19 | 20 | LOCAL_RESOURCES_WRITE_DIR = Path(__file__).parent / "writer_products" 21 | REMOTE_RESOURCES_WRITER_DIR = "s3://aics-modeling-packages-test-resources/fake/dir" 22 | 23 | 24 | def get_resource_full_path(filename: str, host: str) -> Union[str, Path]: 25 | if host is LOCAL: 26 | return LOCAL_RESOURCES_DIR / filename 27 | 28 | return f"{REMOTE_RESOURCES_DIR}/{filename}" 29 | 30 | 31 | def get_resource_write_full_path(filename: str, host: str) -> Union[str, Path]: 32 | if host is LOCAL: 33 | LOCAL_RESOURCES_WRITE_DIR.mkdir(parents=True, exist_ok=True) 34 | return LOCAL_RESOURCES_WRITE_DIR / filename 35 | 36 | return f"{REMOTE_RESOURCES_WRITER_DIR}/{filename}" 37 | 38 | 39 | host = pytest.mark.parametrize("host", [LOCAL]) 40 | 41 | 42 | def np_random_from_shape(shape: Tuple[int, ...], **kwargs: Any) -> np.ndarray: 43 | return np.random.randint(255, size=shape, **kwargs) 44 | 45 | 46 | def da_random_from_shape(shape: Tuple[int, ...], **kwargs: Any) -> da.Array: 47 | return da.random.randint(255, size=shape, **kwargs) 48 | 49 | 50 | array_constructor = pytest.mark.parametrize( 51 | "array_constructor", [np_random_from_shape, da_random_from_shape] 52 | ) 53 | -------------------------------------------------------------------------------- /aicsimageio/tests/metadata/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/extra_readers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/extra_readers/sldy_reader/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/extra_readers/sldy_reader/test_sldy_image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from pathlib import Path 5 | from typing import Optional, Set, Tuple 6 | 7 | import numpy as np 8 | import pytest 9 | 10 | from aicsimageio.readers.sldy_reader.sldy_image import SldyImage 11 | from aicsimageio.utils import io_utils 12 | 13 | from ....conftest import LOCAL, get_resource_full_path 14 | 15 | METADATA_FILES = { 16 | "annotation_record", 17 | "aux_data", 18 | "channel_record", 19 | "elapsed_times", 20 | "image_record", 21 | "mask_record", 22 | "sa_position_data", 23 | "stage_position_data", 24 | } 25 | 26 | 27 | @pytest.mark.parametrize( 28 | "filename, " 29 | "expected_physical_pixel_size_x, " 30 | "expected_physical_pixel_size_y, " 31 | "expected_physical_pixel_size_z, " 32 | "expected_metadata_files, " 33 | "expected_shape, " 34 | "timepoints_to_compare, " 35 | "channels_to_compare, ", 36 | [ 37 | ( 38 | "s1_t10_c1_z5.dir/20220726 endo diff1658874976.imgdir", 39 | 0.38388850322622897, 40 | 0.38388850322622897, 41 | None, 42 | METADATA_FILES, 43 | (5, 1736, 1776), 44 | (0, 1), 45 | (0, 0), 46 | ), 47 | ( 48 | "s1_t1_c2_z40.dir/3500005564_20X_timelapse_202304201682033857.imgdir", 49 | 0.3820158766750814, 50 | 0.3820158766750814, 51 | None, 52 | METADATA_FILES, 53 | (40, 1736, 1776), 54 | (0, 0), 55 | (0, 1), 56 | ), 57 | ], 58 | ) 59 | def test_sldy_image( 60 | filename: str, 61 | expected_physical_pixel_size_x: float, 62 | expected_physical_pixel_size_y: float, 63 | expected_physical_pixel_size_z: Optional[float], 64 | expected_metadata_files: Set[str], 65 | expected_shape: Tuple[int, ...], 66 | timepoints_to_compare: Tuple[int, int], 67 | channels_to_compare: Tuple[int, int], 68 | ) -> None: 69 | # Determine path to file 70 | uri = get_resource_full_path(filename, LOCAL) 71 | fs, path = io_utils.pathlike_to_fs( 72 | uri, 73 | enforce_exists=True, 74 | ) 75 | 76 | # Construct image 77 | image = SldyImage(fs, Path(path), data_file_prefix="ImageData") 78 | 79 | # Assert image properties match expectation 80 | assert expected_physical_pixel_size_x == image.physical_pixel_size_x 81 | assert expected_physical_pixel_size_y == image.physical_pixel_size_y 82 | assert expected_physical_pixel_size_z == image.physical_pixel_size_z 83 | 84 | # Ensure all metadata files are present then ensure all expected 85 | # metadata files have values 86 | assert METADATA_FILES.issubset(image.metadata.keys()) 87 | for key in image.metadata.keys(): 88 | if key in expected_metadata_files: 89 | assert image.metadata[key], f"Metadata file {key} not found" 90 | 91 | # Assert the data retrieved from the image matches the expectation 92 | data = image.get_data( 93 | timepoint=timepoints_to_compare[0], channel=channels_to_compare[0], delayed=True 94 | ) 95 | assert expected_shape == data.shape 96 | assert not np.array_equal( 97 | data, 98 | image.get_data( 99 | timepoint=timepoints_to_compare[1], 100 | channel=channels_to_compare[1], 101 | delayed=True, 102 | ), 103 | ) 104 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/extra_readers/sldy_reader/test_sldy_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Any, List, Tuple, Union 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from aicsimageio import AICSImage, dimensions, exceptions 10 | from aicsimageio.readers import SldyReader 11 | from aicsimageio.tests.image_container_test_utils import run_image_file_checks 12 | 13 | from ....conftest import LOCAL, get_resource_full_path, host 14 | 15 | 16 | @host 17 | @pytest.mark.parametrize( 18 | "filename, " 19 | "set_scene, " 20 | "expected_scenes, " 21 | "expected_shape, " 22 | "expected_dtype, " 23 | "expected_dims_order, " 24 | "expected_channel_names, " 25 | "expected_physical_pixel_sizes", 26 | [ 27 | pytest.param( 28 | "example.png", 29 | None, 30 | None, 31 | None, 32 | None, 33 | None, 34 | None, 35 | None, 36 | marks=pytest.mark.xfail(raises=exceptions.UnsupportedFileFormatError), 37 | ), 38 | ( 39 | "s1_t10_c1_z5.dir", 40 | "20220726 endo diff1658874976", 41 | ("20220726 endo diff1658874976",), 42 | (10, 1, 5, 1736, 1776), 43 | np.uint16, 44 | dimensions.DEFAULT_DIMENSION_ORDER, 45 | ["0"], 46 | (None, 0.38388850322622897, 0.38388850322622897), 47 | ), 48 | ( 49 | "s1_t1_c2_z40.dir", 50 | "3500005564_20X_timelapse_202304201682033857", 51 | ("3500005564_20X_timelapse_202304201682033857",), 52 | (1, 2, 40, 1736, 1776), 53 | np.uint16, 54 | dimensions.DEFAULT_DIMENSION_ORDER, 55 | [ 56 | "0", 57 | "1", 58 | ], 59 | (None, 0.3820158766750814, 0.3820158766750814), 60 | ), 61 | ], 62 | ) 63 | def test_sldy_reader( 64 | filename: str, 65 | host: str, 66 | set_scene: str, 67 | expected_scenes: Tuple[str, ...], 68 | expected_shape: Tuple[int, ...], 69 | expected_dtype: np.dtype, 70 | expected_dims_order: str, 71 | expected_channel_names: List[str], 72 | expected_physical_pixel_sizes: Tuple[float, float, float], 73 | ) -> None: 74 | # Construct full filepath 75 | uri = get_resource_full_path(filename, host) 76 | 77 | # Run checks 78 | run_image_file_checks( 79 | ImageContainer=SldyReader, 80 | image=uri, 81 | set_scene=set_scene, 82 | expected_scenes=expected_scenes, 83 | expected_current_scene=set_scene, 84 | expected_shape=expected_shape, 85 | expected_dtype=expected_dtype, 86 | expected_dims_order=expected_dims_order, 87 | expected_channel_names=expected_channel_names, 88 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 89 | expected_metadata_type=dict, 90 | ) 91 | 92 | 93 | @pytest.mark.parametrize( 94 | "filename, " 95 | "set_scene, " 96 | "expected_scenes, " 97 | "expected_shape, " 98 | "expected_dtype, " 99 | "expected_dims_order, " 100 | "expected_channel_names, " 101 | "expected_physical_pixel_sizes, " 102 | "expected_metadata_type", 103 | [ 104 | ( 105 | "s1_t10_c1_z5.dir", 106 | "20220726 endo diff1658874976", 107 | ("20220726 endo diff1658874976",), 108 | (10, 1, 5, 1736, 1776), 109 | np.uint16, 110 | dimensions.DEFAULT_DIMENSION_ORDER, 111 | ["0"], 112 | (None, 0.38388850322622897, 0.38388850322622897), 113 | dict, 114 | ), 115 | ], 116 | ) 117 | def test_aicsimage( 118 | filename: str, 119 | set_scene: str, 120 | expected_scenes: Tuple[str, ...], 121 | expected_shape: Tuple[int, ...], 122 | expected_dtype: np.dtype, 123 | expected_dims_order: str, 124 | expected_channel_names: List[str], 125 | expected_physical_pixel_sizes: Tuple[float, float, float], 126 | expected_metadata_type: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], 127 | ) -> None: 128 | # Construct full filepath 129 | uri = get_resource_full_path(filename, LOCAL) 130 | 131 | # Run checks 132 | run_image_file_checks( 133 | ImageContainer=AICSImage, 134 | image=uri, 135 | set_scene=set_scene, 136 | expected_scenes=expected_scenes, 137 | expected_current_scene=set_scene, 138 | expected_shape=expected_shape, 139 | expected_dtype=expected_dtype, 140 | expected_dims_order=expected_dims_order, 141 | expected_channel_names=expected_channel_names, 142 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 143 | expected_metadata_type=expected_metadata_type, 144 | ) 145 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/extra_readers/test_dv_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Any, List, Tuple, Union 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from aicsimageio import AICSImage, dimensions, exceptions 10 | from aicsimageio.readers import DVReader 11 | from aicsimageio.tests.image_container_test_utils import run_image_file_checks 12 | 13 | from ...conftest import LOCAL, get_resource_full_path, host 14 | 15 | 16 | @host 17 | @pytest.mark.parametrize( 18 | "filename, " 19 | "set_scene, " 20 | "expected_scenes, " 21 | "expected_shape, " 22 | "expected_dtype, " 23 | "expected_dims_order, " 24 | "expected_channel_names, " 25 | "expected_physical_pixel_sizes", 26 | [ 27 | pytest.param( 28 | "example.txt", 29 | None, 30 | None, 31 | None, 32 | None, 33 | None, 34 | None, 35 | None, 36 | marks=pytest.mark.xfail(raises=exceptions.UnsupportedFileFormatError), 37 | ), 38 | ( 39 | "DV_siRNAi-HeLa_IN_02.r3d_D3D.dv", 40 | "Image:0", 41 | ("Image:0",), 42 | (4, 1, 40, 512, 512), 43 | np.int16, 44 | "CTZYX", 45 | ["360/457", "490/528", "555/617", "640/685"], 46 | (0.20000000298023224, 0.06502940505743027, 0.06502940505743027), 47 | ), 48 | ( 49 | "DV_siRNAi-HeLa_IN_02.r3d", 50 | "Image:0", 51 | ("Image:0",), 52 | (1, 4, 40, 512, 512), 53 | np.dtype(">i2"), 54 | dimensions.DEFAULT_DIMENSION_ORDER, 55 | ["0/0", "0/0", "0/0", "0/0"], 56 | (0.20000000298023224, 0.06502940505743027, 0.06502940505743027), 57 | ), 58 | ], 59 | ) 60 | def test_dv_reader( 61 | filename: str, 62 | host: str, 63 | set_scene: str, 64 | expected_scenes: Tuple[str, ...], 65 | expected_shape: Tuple[int, ...], 66 | expected_dtype: np.dtype, 67 | expected_dims_order: str, 68 | expected_channel_names: List[str], 69 | expected_physical_pixel_sizes: Tuple[float, float, float], 70 | ) -> None: 71 | # Construct full filepath 72 | uri = get_resource_full_path(filename, host) 73 | 74 | # Run checks 75 | run_image_file_checks( 76 | ImageContainer=DVReader, 77 | image=uri, 78 | set_scene=set_scene, 79 | expected_scenes=expected_scenes, 80 | expected_current_scene=set_scene, 81 | expected_shape=expected_shape, 82 | expected_dtype=expected_dtype, 83 | expected_dims_order=expected_dims_order, 84 | expected_channel_names=expected_channel_names, 85 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 86 | expected_metadata_type=dict, 87 | ) 88 | 89 | 90 | @pytest.mark.parametrize( 91 | "filename, " 92 | "set_scene, " 93 | "expected_scenes, " 94 | "expected_shape, " 95 | "expected_dtype, " 96 | "expected_dims_order, " 97 | "expected_channel_names, " 98 | "expected_physical_pixel_sizes, " 99 | "expected_metadata_type", 100 | [ 101 | ( 102 | "DV_siRNAi-HeLa_IN_02.r3d_D3D.dv", 103 | "Image:0", 104 | ("Image:0",), 105 | (1, 4, 40, 512, 512), 106 | np.int16, 107 | dimensions.DEFAULT_DIMENSION_ORDER, 108 | ["360/457", "490/528", "555/617", "640/685"], 109 | (0.20000000298023224, 0.06502940505743027, 0.06502940505743027), 110 | dict, 111 | ), 112 | ], 113 | ) 114 | def test_aicsimage( 115 | filename: str, 116 | set_scene: str, 117 | expected_scenes: Tuple[str, ...], 118 | expected_shape: Tuple[int, ...], 119 | expected_dtype: np.dtype, 120 | expected_dims_order: str, 121 | expected_channel_names: List[str], 122 | expected_physical_pixel_sizes: Tuple[float, float, float], 123 | expected_metadata_type: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], 124 | ) -> None: 125 | # Construct full filepath 126 | uri = get_resource_full_path(filename, LOCAL) 127 | 128 | # Run checks 129 | run_image_file_checks( 130 | ImageContainer=AICSImage, 131 | image=uri, 132 | set_scene=set_scene, 133 | expected_scenes=expected_scenes, 134 | expected_current_scene=set_scene, 135 | expected_shape=expected_shape, 136 | expected_dtype=expected_dtype, 137 | expected_dims_order=expected_dims_order, 138 | expected_channel_names=expected_channel_names, 139 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 140 | expected_metadata_type=expected_metadata_type, 141 | ) 142 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/extra_readers/test_nd2_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Any, List, Tuple, Union 5 | 6 | import numpy as np 7 | import pytest 8 | from ome_types import OME 9 | 10 | from aicsimageio import AICSImage, dimensions, exceptions 11 | from aicsimageio.readers.nd2_reader import ND2Reader 12 | from aicsimageio.tests.image_container_test_utils import run_image_file_checks 13 | 14 | from ...conftest import LOCAL, get_resource_full_path, host 15 | 16 | nd2 = pytest.importorskip("nd2") 17 | 18 | # nd2 0.4.3 and above improves detection of position names 19 | if tuple(int(x) for x in nd2.__version__.split(".")) >= (0, 4, 3): 20 | pos_names = ("point name 1", "point name 2", "point name 3", "point name 4") 21 | else: 22 | pos_names = ("XYPos:0", "XYPos:1", "XYPos:2", "XYPos:3") 23 | 24 | 25 | @host 26 | @pytest.mark.parametrize( 27 | "filename, " 28 | "set_scene, " 29 | "expected_scenes, " 30 | "expected_shape, " 31 | "expected_dtype, " 32 | "expected_dims_order, " 33 | "expected_channel_names, " 34 | "expected_physical_pixel_sizes, " 35 | "expected_metadata_type", 36 | [ 37 | pytest.param( 38 | "example.txt", 39 | None, 40 | None, 41 | None, 42 | None, 43 | None, 44 | None, 45 | None, 46 | None, 47 | marks=pytest.mark.xfail(raises=exceptions.UnsupportedFileFormatError), 48 | ), 49 | pytest.param( 50 | "ND2_aryeh_but3_cont200-1.nd2", 51 | "XYPos:0", 52 | ("XYPos:0", "XYPos:1", "XYPos:2", "XYPos:3", "XYPos:4"), 53 | (1, 2, 1040, 1392), 54 | np.uint16, 55 | "TCYX", 56 | ["20phase", "20xDiO"], 57 | (1, 50, 50), 58 | dict, 59 | ), 60 | ( 61 | "ND2_jonas_header_test2.nd2", 62 | "XYPos:0", 63 | ("XYPos:0",), 64 | (1, 4, 5, 520, 696), 65 | np.uint16, 66 | "CTZYX", 67 | ["Jonas_DIC"], 68 | (0.5, 0.12863494437945, 0.12863494437945), 69 | OME, 70 | ), 71 | ( 72 | "ND2_maxime_BF007.nd2", 73 | "XYPos:0", 74 | ("XYPos:0",), 75 | (1, 156, 164), 76 | np.uint16, 77 | "CYX", 78 | ["405/488/561/633nm"], 79 | (1.0, 0.158389678930686, 0.158389678930686), 80 | OME, 81 | ), 82 | ( 83 | "ND2_dims_p4z5t3c2y32x32.nd2", 84 | pos_names[0], 85 | pos_names, 86 | (3, 5, 2, 32, 32), 87 | np.uint16, 88 | "TZCYX", 89 | ["Widefield Green", "Widefield Red"], 90 | (1.0, 0.652452890023035, 0.652452890023035), 91 | OME, 92 | ), 93 | ( 94 | "ND2_dims_c2y32x32.nd2", 95 | "XYPos:0", 96 | ("XYPos:0",), 97 | (2, 32, 32), 98 | np.uint16, 99 | "CYX", 100 | ["Widefield Green", "Widefield Red"], 101 | (1.0, 0.652452890023035, 0.652452890023035), 102 | OME, 103 | ), 104 | ( 105 | "ND2_dims_p1z5t3c2y32x32.nd2", 106 | "XYPos:0", 107 | ("XYPos:0",), 108 | (3, 5, 2, 32, 32), 109 | np.uint16, 110 | "TZCYX", 111 | ["Widefield Green", "Widefield Red"], 112 | (1.0, 0.652452890023035, 0.652452890023035), 113 | OME, 114 | ), 115 | ( 116 | "ND2_dims_p2z5t3-2c4y32x32.nd2", 117 | pos_names[1], 118 | pos_names[:2], 119 | (5, 5, 4, 32, 32), 120 | np.uint16, 121 | "TZCYX", 122 | ["Widefield Green", "Widefield Red", "Widefield Far-Red", "Brightfield"], 123 | (1.0, 0.652452890023035, 0.652452890023035), 124 | OME, 125 | ), 126 | ( 127 | "ND2_dims_t3c2y32x32.nd2", 128 | "XYPos:0", 129 | ("XYPos:0",), 130 | (3, 2, 32, 32), 131 | np.uint16, 132 | "TCYX", 133 | ["Widefield Green", "Widefield Red"], 134 | (1.0, 0.652452890023035, 0.652452890023035), 135 | OME, 136 | ), 137 | ( 138 | "ND2_dims_rgb_t3p2c2z3x64y64.nd2", 139 | "XYPos:1", 140 | ("XYPos:0", "XYPos:1"), 141 | (3, 3, 2, 32, 32, 3), 142 | np.uint8, 143 | "TZCYXS", 144 | ["Brightfield", "Brightfield"], 145 | (0.01, 0.34285714285714286, 0.34285714285714286), 146 | OME, 147 | ), 148 | ( 149 | "ND2_dims_rgb.nd2", 150 | "XYPos:0", 151 | ("XYPos:0",), 152 | (1, 64, 64, 3), 153 | np.uint8, 154 | "CYXS", 155 | ["Brightfield"], 156 | (1.0, 0.34285714285714286, 0.34285714285714286), 157 | OME, 158 | ), 159 | ], 160 | ) 161 | def test_nd2_reader( 162 | filename: str, 163 | host: str, 164 | set_scene: str, 165 | expected_scenes: Tuple[str, ...], 166 | expected_shape: Tuple[int, ...], 167 | expected_dtype: np.dtype, 168 | expected_dims_order: str, 169 | expected_channel_names: List[str], 170 | expected_physical_pixel_sizes: Tuple[float, float, float], 171 | expected_metadata_type: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], 172 | ) -> None: 173 | # Construct full filepath 174 | uri = get_resource_full_path(filename, host) 175 | 176 | # Run checks 177 | run_image_file_checks( 178 | ImageContainer=ND2Reader, 179 | image=uri, 180 | set_scene=set_scene, 181 | expected_scenes=expected_scenes, 182 | expected_current_scene=set_scene, 183 | expected_shape=expected_shape, 184 | expected_dtype=expected_dtype, 185 | expected_dims_order=expected_dims_order, 186 | expected_channel_names=expected_channel_names, 187 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 188 | expected_metadata_type=expected_metadata_type, 189 | ) 190 | 191 | 192 | @pytest.mark.parametrize( 193 | "filename, " 194 | "set_scene, " 195 | "expected_scenes, " 196 | "expected_shape, " 197 | "expected_dtype, " 198 | "expected_dims_order, " 199 | "expected_channel_names, " 200 | "expected_physical_pixel_sizes, " 201 | "expected_metadata_type", 202 | [ 203 | ( 204 | "ND2_jonas_header_test2.nd2", 205 | "XYPos:0", 206 | ("XYPos:0",), 207 | (4, 1, 5, 520, 696), 208 | np.uint16, 209 | dimensions.DEFAULT_DIMENSION_ORDER, 210 | ["Jonas_DIC"], 211 | (0.5, 0.12863494437945, 0.12863494437945), 212 | OME, 213 | ), 214 | ], 215 | ) 216 | def test_aicsimage( 217 | filename: str, 218 | set_scene: str, 219 | expected_scenes: Tuple[str, ...], 220 | expected_shape: Tuple[int, ...], 221 | expected_dtype: np.dtype, 222 | expected_dims_order: str, 223 | expected_channel_names: List[str], 224 | expected_physical_pixel_sizes: Tuple[float, float, float], 225 | expected_metadata_type: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], 226 | ) -> None: 227 | # Construct full filepath 228 | uri = get_resource_full_path(filename, LOCAL) 229 | 230 | # Run checks 231 | run_image_file_checks( 232 | ImageContainer=AICSImage, 233 | image=uri, 234 | set_scene=set_scene, 235 | expected_scenes=expected_scenes, 236 | expected_current_scene=set_scene, 237 | expected_shape=expected_shape, 238 | expected_dtype=expected_dtype, 239 | expected_dims_order=expected_dims_order, 240 | expected_channel_names=expected_channel_names, 241 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 242 | expected_metadata_type=expected_metadata_type, 243 | ) 244 | 245 | 246 | @pytest.mark.parametrize( 247 | "filename", 248 | [ 249 | "ND2_jonas_header_test2.nd2", 250 | "ND2_maxime_BF007.nd2", 251 | "ND2_dims_p4z5t3c2y32x32.nd2", 252 | ], 253 | ) 254 | def test_ome_metadata(filename: str) -> None: 255 | # Get full filepath 256 | uri = get_resource_full_path(filename, LOCAL) 257 | 258 | # Init image 259 | img = AICSImage(uri) 260 | 261 | # Test the transform 262 | assert isinstance(img.ome_metadata, OME) 263 | 264 | 265 | def test_frame_metadata() -> None: 266 | filename = "ND2_dims_rgb_t3p2c2z3x64y64.nd2" 267 | uri = get_resource_full_path(filename, LOCAL) 268 | rdr = ND2Reader(uri) 269 | rdr.set_scene(0) 270 | assert isinstance( 271 | rdr.xarray_data.attrs["unprocessed"]["frame"], nd2.structures.FrameMetadata 272 | ) 273 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/extra_readers/test_ome_tiled_tiff_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import List, Tuple, Type 5 | 6 | import numpy as np 7 | import pytest 8 | from ome_types import OME 9 | 10 | from aicsimageio import AICSImage, dimensions, exceptions, readers 11 | from aicsimageio.readers.bfio_reader import OmeTiledTiffReader 12 | from aicsimageio.readers.reader import Reader 13 | 14 | from ...conftest import LOCAL, get_resource_full_path, host 15 | from ...image_container_test_utils import run_image_file_checks 16 | 17 | 18 | @host 19 | @pytest.mark.parametrize( 20 | "filename, " 21 | "set_scene, " 22 | "expected_scenes, " 23 | "expected_shape, " 24 | "expected_dtype, " 25 | "expected_dims_order, " 26 | "expected_channel_names, " 27 | "expected_physical_pixel_sizes", 28 | [ 29 | ( 30 | "s_1_t_1_c_1_z_1_ome_tiff_tiles.ome.tif", 31 | "Image:0", 32 | ("Image:0",), 33 | (1, 1, 1, 325, 475), 34 | np.uint16, 35 | dimensions.DEFAULT_DIMENSION_ORDER, 36 | ["Bright"], 37 | (None, 1.0833333333333333, 1.0833333333333333), 38 | ), 39 | ( 40 | "s_1_t_1_c_10_z_1_ome_tiff_tiles.ome.tif", 41 | "Image:0", 42 | ("Image:0",), 43 | (1, 10, 1, 1736, 1776), 44 | np.uint16, 45 | dimensions.DEFAULT_DIMENSION_ORDER, 46 | [f"C:{i}" for i in range(10)], # This is the actual metadata 47 | (None, None, None), 48 | ), 49 | pytest.param( 50 | # This is the same file as the first file, but it is not tiled 51 | # This should throw and error since it is not tiled 52 | "s_1_t_1_c_1_z_1.ome.tiff", 53 | None, 54 | None, 55 | None, 56 | None, 57 | None, 58 | None, 59 | None, 60 | marks=pytest.mark.xfail(raises=exceptions.UnsupportedFileFormatError), 61 | ), 62 | pytest.param( 63 | "example.txt", 64 | None, 65 | None, 66 | None, 67 | None, 68 | None, 69 | None, 70 | None, 71 | marks=pytest.mark.xfail(raises=exceptions.UnsupportedFileFormatError), 72 | ), 73 | pytest.param( 74 | "s_1_t_1_c_2_z_1.lif", 75 | None, 76 | None, 77 | None, 78 | None, 79 | None, 80 | None, 81 | None, 82 | marks=pytest.mark.xfail(raises=exceptions.UnsupportedFileFormatError), 83 | ), 84 | ], 85 | ) 86 | def test_ome_tiff_reader( 87 | filename: str, 88 | host: str, 89 | set_scene: str, 90 | expected_scenes: Tuple[str, ...], 91 | expected_shape: Tuple[int, ...], 92 | expected_dtype: np.dtype, 93 | expected_dims_order: str, 94 | expected_channel_names: List[str], 95 | expected_physical_pixel_sizes: Tuple[float, float, float], 96 | ) -> None: 97 | # Construct full filepath 98 | uri = get_resource_full_path(filename, host) 99 | 100 | # Run checks 101 | run_image_file_checks( 102 | ImageContainer=OmeTiledTiffReader, 103 | image=uri, 104 | set_scene=set_scene, 105 | expected_scenes=expected_scenes, 106 | expected_current_scene=set_scene, 107 | expected_shape=expected_shape, 108 | expected_dtype=expected_dtype, 109 | expected_dims_order=expected_dims_order, 110 | expected_channel_names=expected_channel_names, 111 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 112 | expected_metadata_type=OME, 113 | ) 114 | 115 | 116 | @pytest.mark.parametrize( 117 | "filename, " 118 | "set_scene, " 119 | "expected_scenes, " 120 | "expected_shape, " 121 | "expected_dtype, " 122 | "expected_dims_order, " 123 | "expected_channel_names, " 124 | "expected_physical_pixel_sizes", 125 | [ 126 | ( 127 | "pre-variance-cfe_ome_tiff_tiles.ome.tif", 128 | "Image:0", 129 | ("Image:0",), 130 | (1, 9, 65, 600, 900), 131 | np.uint16, 132 | dimensions.DEFAULT_DIMENSION_ORDER, 133 | [ 134 | "Bright_2", 135 | "EGFP", 136 | "CMDRP", 137 | "H3342", 138 | "SEG_STRUCT", 139 | "SEG_Memb", 140 | "SEG_DNA", 141 | "CON_Memb", 142 | "CON_DNA", 143 | ], 144 | (0.29, 0.10833333333333334, 0.10833333333333334), 145 | ), 146 | ( 147 | "variance-cfe_ome_tiff_tiles.ome.tif", 148 | "Image:0", 149 | ("Image:0",), 150 | (1, 9, 65, 600, 900), 151 | np.uint16, 152 | dimensions.DEFAULT_DIMENSION_ORDER, 153 | [ 154 | "CMDRP", 155 | "EGFP", 156 | "H3342", 157 | "Bright_2", 158 | "SEG_STRUCT", 159 | "SEG_Memb", 160 | "SEG_DNA", 161 | "CON_Memb", 162 | "CON_DNA", 163 | ], 164 | (0.29, 0.10833333333333332, 0.10833333333333332), 165 | ), 166 | ( 167 | "actk_ome_tiff_tiles.ome.tif", 168 | "Image:0", 169 | ("Image:0",), 170 | (1, 6, 65, 233, 345), 171 | np.float64, 172 | dimensions.DEFAULT_DIMENSION_ORDER, 173 | [ 174 | "nucleus_segmentation", 175 | "membrane_segmentation", 176 | "dna", 177 | "membrane", 178 | "structure", 179 | "brightfield", 180 | ], 181 | (0.29, 0.29, 0.29), 182 | ), 183 | ], 184 | ) 185 | def test_ome_tiff_reader_large_files( 186 | filename: str, 187 | set_scene: str, 188 | expected_scenes: Tuple[str, ...], 189 | expected_shape: Tuple[int, ...], 190 | expected_dtype: np.dtype, 191 | expected_dims_order: str, 192 | expected_channel_names: List[str], 193 | expected_physical_pixel_sizes: Tuple[float, float, float], 194 | ) -> None: 195 | # Construct full filepath 196 | uri = get_resource_full_path(filename, LOCAL) 197 | 198 | # Run checks 199 | run_image_file_checks( 200 | ImageContainer=OmeTiledTiffReader, 201 | image=uri, 202 | set_scene=set_scene, 203 | expected_scenes=expected_scenes, 204 | expected_current_scene=set_scene, 205 | expected_shape=expected_shape, 206 | expected_dtype=expected_dtype, 207 | expected_dims_order=expected_dims_order, 208 | expected_channel_names=expected_channel_names, 209 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 210 | expected_metadata_type=OME, 211 | ) 212 | 213 | 214 | @pytest.mark.parametrize( 215 | "filename, expected_reader", 216 | [ 217 | ( 218 | "s_1_t_1_c_1_z_1_ome_tiff_tiles.ome.tif", 219 | readers.OmeTiledTiffReader, 220 | ), 221 | ], 222 | ) 223 | def test_selected_tiff_reader( 224 | filename: str, 225 | expected_reader: Type[Reader], 226 | ) -> None: 227 | # Construct full filepath 228 | uri = get_resource_full_path(filename, LOCAL) 229 | 230 | # Init 231 | img = AICSImage(uri) 232 | 233 | # Assert basics 234 | assert isinstance(img.reader, expected_reader) 235 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/extra_readers/test_ome_zarr_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Any, List, Tuple, Union 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from aicsimageio import AICSImage, dimensions, exceptions 10 | from aicsimageio.readers import OmeZarrReader 11 | from aicsimageio.tests.image_container_test_utils import run_image_file_checks 12 | 13 | from ...conftest import LOCAL, get_resource_full_path, host 14 | 15 | 16 | @host 17 | @pytest.mark.parametrize( 18 | "filename, " 19 | "set_scene, " 20 | "expected_scenes, " 21 | "expected_shape, " 22 | "expected_dtype, " 23 | "expected_dims_order, " 24 | "expected_channel_names, " 25 | "expected_physical_pixel_sizes", 26 | [ 27 | pytest.param( 28 | "example.png", 29 | None, 30 | None, 31 | None, 32 | None, 33 | None, 34 | None, 35 | None, 36 | marks=pytest.mark.xfail(raises=exceptions.UnsupportedFileFormatError), 37 | ), 38 | # General Zarr 39 | ( 40 | "s1_t1_c1_z1_Image_0.zarr", 41 | "s1_t1_c1_z1", 42 | ("s1_t1_c1_z1",), 43 | (1, 1, 1, 7548, 7549), 44 | np.uint8, 45 | dimensions.DEFAULT_DIMENSION_ORDER, 46 | ["Channel:0:0"], 47 | (1.0, 264.5833333333333, 264.5833333333333), 48 | ), 49 | # Complex General Zarr 50 | ( 51 | "s1_t7_c4_z3_Image_0.zarr", 52 | "s1_t7_c4_z3_Image_0", 53 | ("s1_t7_c4_z3_Image_0",), 54 | (7, 4, 3, 1200, 1800), 55 | np.uint16, 56 | dimensions.DEFAULT_DIMENSION_ORDER, 57 | ["C:0", "C:1", "C:2", "C:3"], 58 | (1.0, 1.0, 1.0), 59 | ), 60 | # Test Resolution Constant 61 | ( 62 | "resolution_constant_zyx.zarr", 63 | "resolution_constant_zyx", 64 | ("resolution_constant_zyx",), 65 | (2, 4, 4), 66 | np.int64, 67 | ( 68 | dimensions.DimensionNames.SpatialZ 69 | + dimensions.DimensionNames.SpatialY 70 | + dimensions.DimensionNames.SpatialX 71 | ), 72 | ["Channel:0"], 73 | (0.1, 0.1, 0.1), 74 | ), 75 | # Test TYX 76 | ( 77 | "dimension_handling_tyx.zarr", 78 | "dimension_handling_tyx", 79 | ("dimension_handling_tyx",), 80 | (2, 4, 4), 81 | np.int64, 82 | ( 83 | dimensions.DimensionNames.Time 84 | + dimensions.DimensionNames.SpatialY 85 | + dimensions.DimensionNames.SpatialX 86 | ), 87 | ["Channel:0"], 88 | (None, 1.0, 1.0), 89 | ), 90 | # Test ZYX 91 | ( 92 | "dimension_handling_zyx.zarr", 93 | "dimension_handling_zyx", 94 | ("dimension_handling_zyx",), 95 | (2, 4, 4), 96 | np.int64, 97 | ( 98 | dimensions.DimensionNames.SpatialZ 99 | + dimensions.DimensionNames.SpatialY 100 | + dimensions.DimensionNames.SpatialX 101 | ), 102 | ["Channel:0"], 103 | (1.0, 1.0, 1.0), 104 | ), 105 | # Test TZYX 106 | ( 107 | "dimension_handling_tzyx.zarr", 108 | "dimension_handling_tzyx", 109 | ("dimension_handling_tzyx",), 110 | (2, 2, 4, 4), 111 | np.int64, 112 | ( 113 | dimensions.DimensionNames.Time 114 | + dimensions.DimensionNames.SpatialZ 115 | + dimensions.DimensionNames.SpatialY 116 | + dimensions.DimensionNames.SpatialX 117 | ), 118 | ["Channel:0"], 119 | (1.0, 1.0, 1.0), 120 | ), 121 | ( 122 | "absent_metadata_dims_zyx.zarr", 123 | "absent_metadata_dims_zyx", 124 | ("absent_metadata_dims_zyx",), 125 | (2, 4, 4), 126 | np.int64, 127 | ( 128 | dimensions.DimensionNames.SpatialZ 129 | + dimensions.DimensionNames.SpatialY 130 | + dimensions.DimensionNames.SpatialX 131 | ), 132 | ["Channel:0"], 133 | (1.0, 1.0, 1.0), 134 | ), 135 | ], 136 | ) 137 | def test_ome_zarr_reader( 138 | filename: str, 139 | host: str, 140 | set_scene: str, 141 | expected_scenes: Tuple[str, ...], 142 | expected_shape: Tuple[int, ...], 143 | expected_dtype: np.dtype, 144 | expected_dims_order: str, 145 | expected_channel_names: List[str], 146 | expected_physical_pixel_sizes: Tuple[float, float, float], 147 | ) -> None: 148 | # Construct full filepath 149 | uri = get_resource_full_path(filename, host) 150 | 151 | # Run checks 152 | run_image_file_checks( 153 | ImageContainer=OmeZarrReader, 154 | image=uri, 155 | set_scene=set_scene, 156 | expected_scenes=expected_scenes, 157 | expected_current_scene=set_scene, 158 | expected_shape=expected_shape, 159 | expected_dtype=expected_dtype, 160 | expected_dims_order=expected_dims_order, 161 | expected_channel_names=expected_channel_names, 162 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 163 | expected_metadata_type=dict, 164 | ) 165 | 166 | 167 | @pytest.mark.parametrize( 168 | "filename, " 169 | "set_scene, " 170 | "expected_scenes, " 171 | "expected_shape, " 172 | "expected_dtype, " 173 | "expected_dims_order, " 174 | "expected_channel_names, " 175 | "expected_physical_pixel_sizes, " 176 | "expected_metadata_type", 177 | [ 178 | ( 179 | "s1_t7_c4_z3_Image_0.zarr", 180 | "s1_t7_c4_z3_Image_0", 181 | ("s1_t7_c4_z3_Image_0",), 182 | (7, 4, 3, 1200, 1800), 183 | np.uint16, 184 | dimensions.DEFAULT_DIMENSION_ORDER, 185 | ["C:0", "C:1", "C:2", "C:3"], 186 | (1.0, 1.0, 1.0), 187 | dict, 188 | ), 189 | ], 190 | ) 191 | def test_aicsimage( 192 | filename: str, 193 | set_scene: str, 194 | expected_scenes: Tuple[str, ...], 195 | expected_shape: Tuple[int, ...], 196 | expected_dtype: np.dtype, 197 | expected_dims_order: str, 198 | expected_channel_names: List[str], 199 | expected_physical_pixel_sizes: Tuple[float, float, float], 200 | expected_metadata_type: Union[type, Tuple[Union[type, Tuple[Any, ...]], ...]], 201 | ) -> None: 202 | # Construct full filepath 203 | uri = get_resource_full_path(filename, LOCAL) 204 | 205 | # Run checks 206 | run_image_file_checks( 207 | ImageContainer=AICSImage, 208 | image=uri, 209 | set_scene=set_scene, 210 | expected_scenes=expected_scenes, 211 | expected_current_scene=set_scene, 212 | expected_shape=expected_shape, 213 | expected_dtype=expected_dtype, 214 | expected_dims_order=expected_dims_order, 215 | expected_channel_names=expected_channel_names, 216 | expected_physical_pixel_sizes=expected_physical_pixel_sizes, 217 | expected_metadata_type=expected_metadata_type, 218 | ) 219 | -------------------------------------------------------------------------------- /aicsimageio/tests/readers/test_glob_reader.py: -------------------------------------------------------------------------------- 1 | #! usr/env/bin/python 2 | import os 3 | from itertools import product 4 | from pathlib import Path 5 | from typing import Any 6 | 7 | import numpy as np 8 | import pandas as pd 9 | import pytest 10 | import tifffile as tiff 11 | import xarray as xr 12 | 13 | import aicsimageio 14 | from aicsimageio.readers.tiff_glob_reader import TiffGlobReader 15 | 16 | DATA_SHAPE = (3, 4, 5, 6, 7, 8) # STCZYX 17 | 18 | 19 | def check_values( 20 | reader: aicsimageio.readers.TiffGlobReader, reference: xr.DataArray 21 | ) -> None: 22 | for i, s in enumerate(reader.scenes): 23 | reader.set_scene(s) 24 | assert np.all( 25 | reference.isel(S=i).data == reader.xarray_dask_data.data 26 | ).compute() 27 | assert np.all(reference.isel(S=i).data == reader.xarray_data.data) 28 | 29 | 30 | def make_fake_data_2d(path: Path, as_mm: bool = False) -> xr.DataArray: 31 | """ 32 | Parameters 33 | ---------- 34 | path : [Path] 35 | Folder to save data in 36 | as_mm : [bool] 37 | Whether to save the data in with Micromanager MDA naming conventions. 38 | 39 | Returns 40 | ------- 41 | x_data : [xr.DataArray] 42 | """ 43 | 44 | data = np.arange(np.prod(DATA_SHAPE), dtype="uint16").reshape(DATA_SHAPE) 45 | dims = list("STCZYX") 46 | 47 | x_data = xr.DataArray(data, dims=dims) 48 | 49 | os.mkdir(str(path / "2d_images")) 50 | for s, t, c, z in product(*(range(x) for x in DATA_SHAPE[:4])): 51 | 52 | im = data[s, t, c, z] 53 | if as_mm: 54 | name = f"img_channel{c}_position{s}_time{t}_z{z}.tif" 55 | else: 56 | name = f"S{s}_T{t}_C{c}_Z{z}.tif" 57 | tiff.imwrite( 58 | str(path / "2d_images" / name), 59 | im, 60 | dtype=np.uint16, 61 | ) 62 | return x_data 63 | 64 | 65 | def test_glob_reader_2d(tmp_path: Path) -> None: 66 | reference = make_fake_data_2d(tmp_path) 67 | gr = aicsimageio.readers.TiffGlobReader(str(tmp_path / "2d_images/*.tif")) 68 | 69 | assert gr.xarray_dask_data.data.chunksize == (1, 1) + DATA_SHAPE[-3:] 70 | 71 | check_values(gr, reference) 72 | 73 | 74 | def test_index_alignment(tmp_path: Path) -> None: 75 | # Testing case where user has passed in a list of files 76 | # and a dataframe with non-continuous index 77 | 78 | # use as_mm to have an easily available Indexer function 79 | _ = make_fake_data_2d(tmp_path, as_mm=True) 80 | filenames = np.array(list((tmp_path / "2d_images").glob("*.tif"))) 81 | # print(filenames) 82 | indexer = pd.Series(filenames).apply(TiffGlobReader.MicroManagerIndexer) 83 | 84 | # Keep only some of the Z 85 | # more realistic case is eliminating everything after a given time point 86 | # but this garuntees that our eliminated images will be embedded all through the 87 | # order rather than just at the end 88 | keep = indexer.Z < 5 89 | 90 | indexer = indexer.loc[keep] 91 | 92 | reader = TiffGlobReader(filenames[keep], indexer) 93 | 94 | # check that there are no nans 95 | # nans are a symptom of index misalignment 96 | assert not reader._all_files.isnull().any().any() 97 | 98 | 99 | @pytest.mark.parametrize( 100 | "type_", 101 | [ 102 | list, 103 | pd.Series, 104 | np.array, 105 | # should throw a TypeError instead of an unboundlocal error 106 | pytest.param(bytes, marks=pytest.mark.xfail(raises=TypeError)), 107 | ], 108 | ) 109 | def test_glob_types(type_: Any, tmp_path: Path) -> None: 110 | reference = make_fake_data_2d(tmp_path) 111 | filenames = list((tmp_path / "2d_images").glob("*.tif")) 112 | 113 | gr = TiffGlobReader(type_(filenames)) 114 | check_values(gr, reference) 115 | 116 | 117 | def test_mm_indexer(tmp_path: Path) -> None: 118 | _ = make_fake_data_2d(tmp_path, True) 119 | gr = aicsimageio.readers.TiffGlobReader( 120 | str(tmp_path / "2d_images/*.tif"), indexer=TiffGlobReader.MicroManagerIndexer 121 | ) 122 | assert gr.dims.order == "TCZYX" 123 | assert gr.dims.shape == DATA_SHAPE[1:] 124 | 125 | 126 | def make_fake_data_3d(path: Path) -> xr.DataArray: 127 | 128 | data = np.arange(np.prod(DATA_SHAPE), dtype="uint16").reshape(DATA_SHAPE) 129 | 130 | dims = list("STCZYX") 131 | 132 | x_data = xr.DataArray(data, dims=dims) 133 | 134 | os.mkdir(str(path / "3d_images")) 135 | 136 | shape_for_3d = (*DATA_SHAPE[:3], int(DATA_SHAPE[3] / 2)) 137 | for s, t, c, z in product(*(range(x) for x in shape_for_3d)): 138 | im = data[s, t, c, 2 * z : 2 * (z + 1)] 139 | tiff.imwrite( 140 | str(path / f"3d_images/S{s}_T{t}_C{c}_Z{z}.tif"), 141 | im, 142 | dtype=np.uint16, 143 | ) 144 | return x_data 145 | 146 | 147 | def test_glob_reader_3d(tmp_path: Path) -> None: 148 | reference = make_fake_data_3d(tmp_path) 149 | 150 | # do not stack z dimension 151 | gr = aicsimageio.readers.TiffGlobReader( 152 | str(tmp_path / "3d_images/*Z0.tif"), single_file_dims=list("ZYX") 153 | ) 154 | assert gr.xarray_dask_data.data.chunksize == (1, 1, 2, 7, 8) 155 | check_values(gr, reference.isel(Z=slice(0, 2))) 156 | 157 | # stack along z dimension but do not chunk 158 | gr = aicsimageio.readers.TiffGlobReader( 159 | str(tmp_path / "3d_images/*.tif"), 160 | single_file_dims=list("ZYX"), 161 | chunk_dims=list("TC"), 162 | ) 163 | assert gr.xarray_dask_data.data.chunksize == (4, 5, 2, 7, 8) 164 | check_values(gr, reference) 165 | 166 | # stack along z and chunk along z 167 | gr = aicsimageio.readers.TiffGlobReader( 168 | str(tmp_path / "3d_images/*.tif"), single_file_dims=list("ZYX") 169 | ) 170 | assert gr.xarray_dask_data.data.chunksize == (1, 1, 6, 7, 8) 171 | check_values(gr, reference) 172 | 173 | 174 | def make_fake_data_4d(path: Path) -> xr.DataArray: 175 | 176 | data = np.arange(np.prod(DATA_SHAPE), dtype="uint16").reshape(DATA_SHAPE) 177 | 178 | dims = list("STCZYX") 179 | 180 | x_data = xr.DataArray(data, dims=dims) 181 | 182 | os.mkdir(str(path / "4d_images")) 183 | 184 | per_file_t = 2 185 | t_files = int(DATA_SHAPE[1] / per_file_t) 186 | 187 | per_file_z = 3 188 | z_files = int(DATA_SHAPE[3] / per_file_z) 189 | 190 | shape_for_4d = (DATA_SHAPE[0], t_files, DATA_SHAPE[2], z_files) 191 | for s, t, c, z in product(*(range(x) for x in shape_for_4d)): 192 | im = data[ 193 | s, 194 | per_file_t * t : per_file_t * (t + 1), 195 | c, 196 | per_file_z * z : per_file_z * (z + 1), 197 | ] 198 | tiff.imwrite( 199 | str(path / f"4d_images/S{s}_T{t}_C{c}_Z{z}.tif"), 200 | im, 201 | dtype=np.uint16, 202 | photometric="MINISBLACK", 203 | ) 204 | return x_data 205 | 206 | 207 | def test_glob_reader_4d(tmp_path: Path) -> None: 208 | reference = make_fake_data_4d(tmp_path) 209 | 210 | # stack none 211 | gr = aicsimageio.readers.TiffGlobReader( 212 | str(tmp_path / "4d_images/*T0*Z0.tif"), single_file_dims=list("TZYX") 213 | ) 214 | assert gr.xarray_dask_data.data.chunksize == (2, 1, 3, 7, 8) 215 | check_values(gr, reference.isel(T=slice(0, 2), Z=slice(0, 3))) 216 | 217 | # stack z and t - chunk z 218 | gr = aicsimageio.readers.TiffGlobReader( 219 | str(tmp_path / "4d_images/*.tif"), single_file_dims=list("TZYX") 220 | ) 221 | assert gr.xarray_dask_data.data.chunksize == (2, 1, 6, 7, 8) 222 | check_values(gr, reference) 223 | 224 | # stack z and t - chunk z and t 225 | gr = aicsimageio.readers.TiffGlobReader( 226 | str(tmp_path / "4d_images/*.tif"), 227 | single_file_dims=list("TZYX"), 228 | chunk_dims=["T", "Z"], 229 | ) 230 | assert gr.xarray_dask_data.data.chunksize == (4, 1, 6, 7, 8) 231 | check_values(gr, reference) 232 | 233 | # stack z an t - chunk ztc 234 | gr = aicsimageio.readers.TiffGlobReader( 235 | str(tmp_path / "4d_images/*.tif"), 236 | single_file_dims=list("TZYX"), 237 | chunk_dims=list("TCZ"), 238 | ) 239 | assert gr.xarray_dask_data.data.chunksize == (4, 5, 6, 7, 8) 240 | check_values(gr, reference) 241 | 242 | 243 | def test_aics_image(tmp_path: Path) -> None: 244 | 245 | _ = make_fake_data_4d(tmp_path) 246 | 247 | aicsimage_tiff = aicsimageio.AICSImage(tmp_path / "4d_images/S0_T0_C0_Z0.tif") 248 | assert isinstance(aicsimage_tiff.reader, aicsimageio.readers.tiff_reader.TiffReader) 249 | 250 | aicsimage_tiff_glob = aicsimageio.AICSImage( 251 | tmp_path / "4d_images/*.tif", single_file_dims=list("TZYX") 252 | ) 253 | assert isinstance(aicsimage_tiff_glob.reader, aicsimageio.readers.TiffGlobReader) 254 | -------------------------------------------------------------------------------- /aicsimageio/tests/test_aics_image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import pytest 5 | 6 | from aicsimageio import AICSImage, exceptions 7 | 8 | from .conftest import LOCAL, get_resource_full_path 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "filename", 13 | [ 14 | pytest.param( 15 | "example.txt", 16 | marks=pytest.mark.xfail(raises=exceptions.UnsupportedFileFormatError), 17 | ), 18 | pytest.param( 19 | "does-not-exist-klafjjksdafkjl.bad", 20 | marks=pytest.mark.xfail(raises=FileNotFoundError), 21 | ), 22 | ], 23 | ) 24 | def test_aicsimage( 25 | filename: str, 26 | ) -> None: 27 | # Construct full filepath 28 | uri = get_resource_full_path(filename, LOCAL) 29 | AICSImage(uri) 30 | -------------------------------------------------------------------------------- /aicsimageio/tests/test_dimensions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Collection, Tuple 5 | 6 | import pytest 7 | 8 | from aicsimageio.dimensions import Dimensions 9 | 10 | 11 | def test_dimensions_getitem() -> None: 12 | dims = Dimensions("TCZYX", (1, 4, 75, 624, 924)) 13 | assert dims["T"] == (1,) 14 | assert dims["T", "C"] == (1, 4) 15 | 16 | # out of order indexing 17 | assert dims["C", "T", "Y"] == (4, 1, 624) 18 | 19 | with pytest.raises(IndexError): 20 | dims["blarg"] 21 | with pytest.raises(IndexError): 22 | dims["blarg", "nope"] 23 | with pytest.raises(TypeError): 24 | # Ironic we have to type ignore this because uhhh 25 | # we are testing a TypeError 26 | dims[0] # type: ignore 27 | 28 | assert dims.T == 1 29 | assert dims.order == "TCZYX" 30 | 31 | 32 | @pytest.mark.parametrize( 33 | "dims, shape", 34 | [ 35 | (["Z", "Y", "X"], (70, 980, 980)), 36 | pytest.param( 37 | "ZYXS", 38 | (70, 980, 980), 39 | marks=pytest.mark.xfail(raises=ValueError), 40 | ), 41 | pytest.param( 42 | "YX", 43 | (70, 980, 980), 44 | marks=pytest.mark.xfail(raises=ValueError), 45 | ), 46 | ], 47 | ) 48 | def test_dimensions_mismatched_dims_len_and_shape_size( 49 | dims: Collection[str], 50 | shape: Tuple[int, ...], 51 | ) -> None: 52 | # Just check success 53 | assert Dimensions(dims, shape) 54 | 55 | 56 | @pytest.mark.parametrize( 57 | "dims, shape", 58 | [ 59 | (["Z", "Y", "X"], (70, 980, 980)), 60 | pytest.param( 61 | ["C", "ZY", "X"], 62 | (70, 980, 980), 63 | marks=pytest.mark.xfail(raises=ValueError), 64 | ), 65 | pytest.param( 66 | ["YX"], 67 | (70, 980, 980), 68 | marks=pytest.mark.xfail(raises=ValueError), 69 | ), 70 | ], 71 | ) 72 | def test_dimensions_bad_iterable_of_characters( 73 | dims: Collection[str], 74 | shape: Tuple[int, ...], 75 | ) -> None: 76 | # Just check success 77 | assert Dimensions(dims, shape) 78 | -------------------------------------------------------------------------------- /aicsimageio/tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/tests/utils/test_io_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import pytest 5 | 6 | from aicsimageio.utils.io_utils import pathlike_to_fs 7 | 8 | from ..conftest import LOCAL, REMOTE, get_resource_full_path 9 | 10 | 11 | @pytest.mark.parametrize("host", [LOCAL, REMOTE]) 12 | @pytest.mark.parametrize( 13 | "filename, enforce_exists", 14 | [ 15 | ("example.txt", False), 16 | ("example.txt", False), 17 | ("does-not-exist.good", False), 18 | ("does-not-exist.good", False), 19 | pytest.param( 20 | "does-not-exist.bad", 21 | True, 22 | marks=pytest.mark.xfail(raises=FileNotFoundError), 23 | ), 24 | pytest.param( 25 | "does-not-exist.bad", 26 | True, 27 | marks=pytest.mark.xfail(raises=FileNotFoundError), 28 | ), 29 | ], 30 | ) 31 | def test_pathlike_to_fs(filename: str, host: str, enforce_exists: bool) -> None: 32 | # Construct full filepath 33 | uri = get_resource_full_path(filename, host) 34 | 35 | pathlike_to_fs(uri, enforce_exists, fs_kwargs=dict(anon=True)) 36 | -------------------------------------------------------------------------------- /aicsimageio/tests/writers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/tests/writers/extra_writers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/tests/writers/extra_writers/test_timeseries_writer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Callable, Tuple 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from aicsimageio import exceptions 10 | from aicsimageio.readers.default_reader import DefaultReader 11 | from aicsimageio.writers.timeseries_writer import TimeseriesWriter 12 | 13 | from ...conftest import LOCAL, array_constructor, get_resource_write_full_path 14 | 15 | 16 | @array_constructor 17 | @pytest.mark.parametrize( 18 | "write_shape, write_dim_order, read_shape, read_dim_order", 19 | [ 20 | ((30, 100, 100), None, (30, 100, 100, 3), "TYXS"), 21 | # Note that files get saved out with RGBA, instead of just RGB 22 | ((30, 100, 100, 3), None, (30, 100, 100, 3), "TYXS"), 23 | ((100, 30, 100), "XTY", (30, 100, 100, 3), "TYXS"), 24 | # Note that files get saved out with RGBA, instead of just RGB 25 | ((3, 100, 30, 100), "SYTX", (30, 100, 100, 3), "TYXS"), 26 | pytest.param( 27 | (1, 1), 28 | None, 29 | None, 30 | None, 31 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 32 | ), 33 | pytest.param( 34 | (1, 1, 1, 1, 1), 35 | None, 36 | None, 37 | None, 38 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 39 | ), 40 | pytest.param( 41 | (1, 1, 1, 1, 1, 1), 42 | "STCZYX", 43 | None, 44 | None, 45 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 46 | ), 47 | pytest.param( 48 | (1, 1, 1, 1), 49 | "ABCD", 50 | None, 51 | None, 52 | marks=pytest.mark.xfail(raises=exceptions.InvalidDimensionOrderingError), 53 | ), 54 | ], 55 | ) 56 | @pytest.mark.parametrize("filename", ["e.gif"]) 57 | def test_timeseries_writer( 58 | array_constructor: Callable, 59 | write_shape: Tuple[int, ...], 60 | write_dim_order: str, 61 | read_shape: Tuple[int, ...], 62 | read_dim_order: str, 63 | filename: str, 64 | ) -> None: 65 | # Create array 66 | arr = array_constructor(write_shape, dtype=np.uint8) 67 | 68 | # Construct save end point 69 | save_uri = get_resource_write_full_path(filename, LOCAL) 70 | 71 | # Normal save 72 | TimeseriesWriter.save(arr, save_uri, write_dim_order) 73 | 74 | # Read written result and check basics 75 | reader = DefaultReader(save_uri) 76 | 77 | # Check basics 78 | assert reader.shape == read_shape 79 | assert reader.dims.order == read_dim_order 80 | 81 | # Can't do "easy" testing because compression + shape mismatches on RGB data 82 | 83 | 84 | @array_constructor 85 | @pytest.mark.parametrize( 86 | "write_shape, write_dim_order, read_shape, read_dim_order", 87 | [ 88 | # We use 112 instead of 100 because FFMPEG block size warnings are annoying 89 | ((30, 112, 112), None, (30, 112, 112, 3), "TYXS"), 90 | # Note that files get saved out with RGBA, instead of just RGB 91 | ((30, 112, 112, 3), None, (30, 112, 112, 3), "TYXS"), 92 | ((112, 30, 112), "XTY", (30, 112, 112, 3), "TYXS"), 93 | # Note that files get saved out with RGBA, instead of just RGB 94 | ((3, 112, 30, 112), "SYTX", (30, 112, 112, 3), "TYXS"), 95 | pytest.param( 96 | (1, 1), 97 | None, 98 | None, 99 | None, 100 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 101 | ), 102 | pytest.param( 103 | (1, 1, 1, 1, 1), 104 | None, 105 | None, 106 | None, 107 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 108 | ), 109 | pytest.param( 110 | (1, 1, 1, 1, 1, 1), 111 | "STCZYX", 112 | None, 113 | None, 114 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 115 | ), 116 | pytest.param( 117 | (1, 1, 1, 1), 118 | "ABCD", 119 | None, 120 | None, 121 | marks=pytest.mark.xfail(raises=exceptions.InvalidDimensionOrderingError), 122 | ), 123 | ], 124 | ) 125 | @pytest.mark.parametrize("filename", ["f.mp4"]) 126 | def test_timeseries_writer_ffmpeg( 127 | array_constructor: Callable, 128 | write_shape: Tuple[int, ...], 129 | write_dim_order: str, 130 | read_shape: Tuple[int, ...], 131 | read_dim_order: str, 132 | filename: str, 133 | ) -> None: 134 | # Create array 135 | arr = array_constructor(write_shape, dtype=np.uint8) 136 | 137 | # Construct save end point 138 | save_uri = get_resource_write_full_path(filename, LOCAL) 139 | 140 | # Catch invalid save 141 | # if host == REMOTE: 142 | # with pytest.raises(IOError): 143 | # TimeseriesWriter.save(arr, save_uri, write_dim_order) 144 | 145 | # return 146 | 147 | # Normal save 148 | TimeseriesWriter.save(arr, save_uri, write_dim_order) 149 | 150 | # Read written result and check basics 151 | reader = DefaultReader(save_uri) 152 | 153 | # Check basics 154 | assert reader.shape == read_shape 155 | assert reader.dims.order == read_dim_order 156 | 157 | # Can't do "easy" testing because compression + shape mismatches on RGB data 158 | -------------------------------------------------------------------------------- /aicsimageio/tests/writers/extra_writers/test_two_d_writer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Callable, Tuple 5 | 6 | import numpy as np 7 | import pytest 8 | 9 | from aicsimageio import exceptions 10 | from aicsimageio.readers.default_reader import DefaultReader 11 | from aicsimageio.writers.two_d_writer import TwoDWriter 12 | 13 | from ...conftest import LOCAL, array_constructor, get_resource_write_full_path 14 | 15 | 16 | @array_constructor 17 | @pytest.mark.parametrize( 18 | "write_shape, write_dim_order, read_shape, read_dim_order", 19 | [ 20 | ((100, 100, 3), None, (100, 100, 3), "YXS"), 21 | ((100, 100), None, (100, 100), "YX"), 22 | ((100, 100), "XY", (100, 100), "YX"), 23 | ((3, 100, 100), "SYX", (100, 100, 3), "YXS"), 24 | ((100, 3, 100), "XSY", (100, 100, 3), "YXS"), 25 | pytest.param( 26 | (1, 1, 1, 1), 27 | None, 28 | None, 29 | None, 30 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 31 | ), 32 | pytest.param( 33 | (1, 1, 1, 1, 1), 34 | None, 35 | None, 36 | None, 37 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 38 | ), 39 | pytest.param( 40 | (1, 1, 1, 1, 1, 1), 41 | "STCZYX", 42 | None, 43 | None, 44 | marks=pytest.mark.xfail(raises=exceptions.UnexpectedShapeError), 45 | ), 46 | pytest.param( 47 | (1, 1), 48 | "AB", 49 | None, 50 | None, 51 | marks=pytest.mark.xfail(raises=exceptions.InvalidDimensionOrderingError), 52 | ), 53 | ], 54 | ) 55 | @pytest.mark.parametrize("filename", ["a.png", "d.bmp"]) 56 | def test_two_d_writer( 57 | array_constructor: Callable, 58 | write_shape: Tuple[int, ...], 59 | write_dim_order: str, 60 | read_shape: Tuple[int, ...], 61 | read_dim_order: str, 62 | filename: str, 63 | ) -> None: 64 | # Create array 65 | arr = array_constructor(write_shape, dtype=np.uint8) 66 | 67 | # Construct save end point 68 | save_uri = get_resource_write_full_path(filename, LOCAL) 69 | 70 | # Save 71 | TwoDWriter.save(arr, save_uri, write_dim_order) 72 | 73 | # Read written result and check basics 74 | reader = DefaultReader(save_uri) 75 | 76 | # Check basics 77 | assert reader.shape == read_shape 78 | assert reader.dims.order == read_dim_order 79 | 80 | # We want to check the arrays equal 81 | # but remember, the reader returns data in standard read order 82 | # so we need to get it back as write order 83 | np.testing.assert_array_equal(arr, reader.get_image_data(write_dim_order)) 84 | -------------------------------------------------------------------------------- /aicsimageio/tests/writers/test_ome_zarr_writer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import pathlib 5 | from typing import Callable, List, Optional, Tuple 6 | 7 | import numpy as np 8 | import pytest 9 | from ome_zarr.io import parse_url 10 | from ome_zarr.reader import Reader 11 | 12 | from aicsimageio import exceptions 13 | from aicsimageio.writers import OmeZarrWriter 14 | 15 | from ..conftest import array_constructor 16 | 17 | 18 | @array_constructor 19 | @pytest.mark.parametrize( 20 | "write_shape, write_dim_order, expected_read_shape, expected_read_dim_order", 21 | [ 22 | ((1, 2, 3, 4, 5), None, (1, 2, 3, 4, 5), "TCZYX"), 23 | ((1, 2, 3, 4, 5), "TCZYX", (1, 2, 3, 4, 5), "TCZYX"), 24 | ((2, 3, 4, 5, 6), None, (2, 3, 4, 5, 6), "TCZYX"), 25 | ((1, 1, 1, 1, 1), None, (1, 1, 1, 1, 1), "TCZYX"), 26 | ((5, 16, 16), None, (5, 16, 16), "ZYX"), 27 | ((5, 16, 16), "ZYX", (5, 16, 16), "ZYX"), 28 | ((5, 16, 16), "CYX", (5, 16, 16), "CYX"), 29 | ((5, 16, 16), "TYX", (5, 16, 16), "TYX"), 30 | pytest.param( 31 | (10, 5, 16, 16), 32 | "ZCYX", 33 | (10, 5, 16, 16), 34 | "ZCYX", 35 | marks=pytest.mark.xfail(raises=exceptions.InvalidDimensionOrderingError), 36 | ), 37 | ((5, 10, 16, 16), "CZYX", (5, 10, 16, 16), "CZYX"), 38 | ((15, 16), "YX", (15, 16), "YX"), 39 | pytest.param( 40 | (2, 3, 3), 41 | "AYX", 42 | None, 43 | None, 44 | marks=pytest.mark.xfail(raises=exceptions.InvalidDimensionOrderingError), 45 | ), 46 | ((2, 3, 3), "YXZ", (2, 3, 3), "YXZ"), 47 | pytest.param( 48 | (2, 5, 16, 16), 49 | "CYX", 50 | None, 51 | None, 52 | marks=pytest.mark.xfail(raises=exceptions.InvalidDimensionOrderingError), 53 | ), 54 | # error 6D data doesn't work yet 55 | pytest.param( 56 | (1, 2, 3, 4, 5, 3), 57 | None, 58 | None, 59 | None, 60 | marks=pytest.mark.xfail(raises=exceptions.InvalidDimensionOrderingError), 61 | ), 62 | ], 63 | ) 64 | @pytest.mark.parametrize("filename", ["e.zarr"]) 65 | def test_ome_zarr_writer_dims( 66 | array_constructor: Callable, 67 | write_shape: Tuple[int, ...], 68 | write_dim_order: Optional[str], 69 | expected_read_shape: Tuple[int, ...], 70 | expected_read_dim_order: str, 71 | filename: str, 72 | tmpdir: pathlib.Path, 73 | ) -> None: 74 | # Create array 75 | arr = array_constructor(write_shape, dtype=np.uint8) 76 | 77 | save_uri = str(tmpdir / filename) 78 | 79 | # Normal save 80 | writer = OmeZarrWriter(save_uri) 81 | writer.write_image(arr, "", None, None, None, dimension_order=write_dim_order) 82 | 83 | # Read written result and check basics 84 | reader = Reader(parse_url(save_uri)) 85 | node = list(reader())[0] 86 | num_levels = len(node.data) 87 | assert num_levels == 1 88 | level = 0 89 | shape = node.data[level].shape 90 | assert shape == expected_read_shape 91 | axes = node.metadata["axes"] 92 | dims = "".join([a["name"] for a in axes]).upper() 93 | assert dims == expected_read_dim_order 94 | 95 | 96 | @array_constructor 97 | @pytest.mark.parametrize( 98 | "write_shape, num_levels, scale, expected_read_shapes, expected_read_scales", 99 | [ 100 | ( 101 | (2, 4, 8, 16, 32), 102 | 2, 103 | 2, 104 | [(2, 4, 8, 16, 32), (2, 4, 8, 8, 16), (2, 4, 8, 4, 8)], 105 | [ 106 | [1.0, 1.0, 1.0, 1.0, 1.0], 107 | [1.0, 1.0, 1.0, 2.0, 2.0], 108 | [1.0, 1.0, 1.0, 4.0, 4.0], 109 | ], 110 | ), 111 | ( 112 | (16, 32), 113 | 2, 114 | 4, 115 | [(16, 32), (4, 8), (1, 2)], 116 | [ 117 | [1.0, 1.0], 118 | [4.0, 4.0], 119 | [16.0, 16.0], 120 | ], 121 | ), 122 | ], 123 | ) 124 | @pytest.mark.parametrize("filename", ["f.zarr"]) 125 | def test_ome_zarr_writer_scaling( 126 | array_constructor: Callable, 127 | write_shape: Tuple[int, ...], 128 | num_levels: int, 129 | scale: float, 130 | expected_read_shapes: List[Tuple[int, ...]], 131 | expected_read_scales: List[List[int]], 132 | filename: str, 133 | tmpdir: pathlib.Path, 134 | ) -> None: 135 | # Create array 136 | arr = array_constructor(write_shape, dtype=np.uint8) 137 | save_uri = str(tmpdir / filename) 138 | 139 | # Normal save 140 | writer = OmeZarrWriter(save_uri) 141 | writer.write_image( 142 | arr, "", None, None, None, scale_num_levels=num_levels, scale_factor=scale 143 | ) 144 | 145 | # Read written result and check basics 146 | reader = Reader(parse_url(save_uri)) 147 | node = list(reader())[0] 148 | read_num_levels = len(node.data) 149 | assert num_levels == read_num_levels 150 | print(node.metadata) 151 | for i in range(num_levels): 152 | shape = node.data[i].shape 153 | assert shape == expected_read_shapes[i] 154 | xforms = node.metadata["coordinateTransformations"][i] 155 | assert len(xforms) == 1 156 | assert xforms[0]["type"] == "scale" 157 | assert xforms[0]["scale"] == expected_read_scales[i] 158 | 159 | 160 | @array_constructor 161 | @pytest.mark.parametrize( 162 | "write_shape, chunk_dims, num_levels, expected_read_shapes", 163 | [ 164 | ( 165 | (2, 4, 8, 16, 32), 166 | (1, 1, 2, 16, 16), 167 | 2, 168 | [(2, 4, 8, 16, 32), (2, 4, 8, 8, 16), (2, 4, 8, 4, 8)], 169 | ), 170 | ( 171 | (16, 32), 172 | (2, 4), 173 | 2, 174 | [(16, 32), (8, 16), (4, 8)], 175 | ), 176 | ], 177 | ) 178 | @pytest.mark.parametrize("filename", ["e.zarr"]) 179 | def test_ome_zarr_writer_chunks( 180 | array_constructor: Callable, 181 | write_shape: Tuple[int, ...], 182 | chunk_dims: Tuple[int, ...], 183 | num_levels: int, 184 | filename: str, 185 | expected_read_shapes: List[Tuple[int, ...]], 186 | tmpdir: pathlib.Path, 187 | ) -> None: 188 | arr = array_constructor(write_shape, dtype=np.uint8) 189 | 190 | # Construct save end point 191 | 192 | baseline_save_uri = str(tmpdir / f"baseline_{filename}") 193 | save_uri = str(tmpdir / filename) 194 | 195 | # Normal save 196 | writer = OmeZarrWriter(save_uri) 197 | writer.write_image( 198 | arr, "", None, None, None, chunk_dims=chunk_dims, scale_num_levels=num_levels 199 | ) 200 | reader = Reader(parse_url(save_uri)) 201 | node = list(reader())[0] 202 | 203 | # Check expected shapes 204 | for level in range(num_levels): 205 | shape = node.data[level].shape 206 | assert shape == expected_read_shapes[level] 207 | 208 | # Create baseline chunking to compare against manual. 209 | writer = OmeZarrWriter(baseline_save_uri) 210 | writer.write_image(arr, "", None, None, None, scale_num_levels=num_levels) 211 | reader_baseline = Reader(parse_url(baseline_save_uri)) 212 | node_baseline = list(reader_baseline())[0] 213 | 214 | data = node.data[0] 215 | baseline_data = node_baseline.data[0] 216 | 217 | assert np.all(np.equal(data, baseline_data)) 218 | -------------------------------------------------------------------------------- /aicsimageio/types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from pathlib import Path 5 | from typing import List, NamedTuple, Optional, Union 6 | 7 | import dask.array as da 8 | import numpy as np 9 | import xarray as xr 10 | 11 | ############################################################################### 12 | 13 | # IO Types 14 | PathLike = Union[str, Path] 15 | ArrayLike = Union[np.ndarray, da.Array] 16 | MetaArrayLike = Union[ArrayLike, xr.DataArray] 17 | ImageLike = Union[ 18 | PathLike, ArrayLike, MetaArrayLike, List[MetaArrayLike], List[PathLike] 19 | ] 20 | 21 | 22 | # Image Utility Types 23 | class PhysicalPixelSizes(NamedTuple): 24 | Z: Optional[float] 25 | Y: Optional[float] 26 | X: Optional[float] 27 | -------------------------------------------------------------------------------- /aicsimageio/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /aicsimageio/utils/io_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from pathlib import Path 5 | from typing import Any, Dict, Tuple 6 | 7 | from fsspec.core import url_to_fs 8 | from fsspec.spec import AbstractFileSystem 9 | 10 | from ..types import PathLike 11 | 12 | ############################################################################### 13 | 14 | 15 | def pathlike_to_fs( 16 | uri: PathLike, 17 | enforce_exists: bool = False, 18 | fs_kwargs: Dict[str, Any] = {}, 19 | ) -> Tuple[AbstractFileSystem, str]: 20 | """ 21 | Find and return the appropriate filesystem and path from a path-like object. 22 | 23 | Parameters 24 | ---------- 25 | uri: PathLike 26 | The local or remote path or uri. 27 | enforce_exists: bool 28 | Check whether or not the resource exists, if not, raise FileNotFoundError. 29 | 30 | Returns 31 | ------- 32 | fs: AbstractFileSystem 33 | The filesystem to operate on. 34 | path: str 35 | The full path to the target resource. 36 | fs_kwargs: Dict[str, Any] 37 | Any specific keyword arguments to pass down to the fsspec created filesystem. 38 | Default: {} 39 | 40 | Raises 41 | ------ 42 | FileNotFoundError 43 | If enforce_exists is provided value True and the resource is not found or is 44 | unavailable. 45 | """ 46 | # Convert paths to string to be handled by url_to_fs 47 | if isinstance(uri, Path): 48 | uri = str(uri) 49 | 50 | # Get details 51 | fs, path = url_to_fs(uri, **fs_kwargs) 52 | 53 | # Check file exists 54 | if enforce_exists: 55 | if not fs.exists(path): 56 | raise FileNotFoundError(f"{fs.protocol}://{path}") 57 | 58 | # Get and store details 59 | # We do not return an AbstractBufferedFile (i.e. fs.open) as we do not want to have 60 | # any open file buffers _after_ any API call. API calls must themselves call 61 | # fs.open and complete their function during the context of the opened buffer. 62 | return fs, path 63 | -------------------------------------------------------------------------------- /aicsimageio/writers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .ome_tiff_writer import OmeTiffWriter # noqa: F401 5 | from .ome_zarr_writer import OmeZarrWriter # noqa: F401 6 | -------------------------------------------------------------------------------- /aicsimageio/writers/timeseries_writer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Any, Dict 5 | 6 | import dask.array as da 7 | import numpy as np 8 | from fsspec.implementations.local import LocalFileSystem 9 | from imageio import get_writer 10 | 11 | from .. import types 12 | from ..dimensions import DimensionNames 13 | from ..exceptions import InvalidDimensionOrderingError, UnexpectedShapeError 14 | from ..transforms import reshape_data 15 | from ..utils import io_utils 16 | from .writer import Writer 17 | 18 | try: 19 | from ..readers.default_reader import DefaultReader 20 | 21 | except ImportError: 22 | raise ImportError( 23 | "Base imageio is required for this writer. " 24 | "Install with `pip install aicsimageio[base-imageio]`" 25 | ) 26 | 27 | 28 | ############################################################################### 29 | 30 | 31 | class TimeseriesWriter(Writer): 32 | """ 33 | A writer for timeseries Greyscale, RGB, or RGBA image data. 34 | Primarily directed at formats: "gif", "mp4", "mkv", etc. 35 | 36 | Notes 37 | ----- 38 | To use this writer, install with: `pip install aicsimageio[base-imageio]`. 39 | """ 40 | 41 | _TIMEPOINT_DIMENSIONS = [ 42 | DimensionNames.Time, 43 | DimensionNames.SpatialY, 44 | DimensionNames.SpatialX, 45 | ] 46 | _TIMEPOINT_WITH_SAMPLES_DIMENSIONS = _TIMEPOINT_DIMENSIONS + [ 47 | DimensionNames.Samples 48 | ] 49 | 50 | DIM_ORDERS = { 51 | 3: "".join(_TIMEPOINT_DIMENSIONS), # Greyscale 52 | 4: "".join(_TIMEPOINT_WITH_SAMPLES_DIMENSIONS), # RGB / RGBA 53 | } 54 | 55 | @staticmethod 56 | def _write_chunks( 57 | f: str, 58 | extension: str, 59 | imageio_mode: str, 60 | fps: int, 61 | data: da.Array, 62 | dim_order: str, 63 | ) -> None: 64 | with get_writer( 65 | f, 66 | format=extension, 67 | mode=imageio_mode, 68 | fps=fps, 69 | ) as writer: 70 | # Make each chunk of the dask array be a frame 71 | chunks = tuple(1 if dim == DimensionNames.Time else -1 for dim in dim_order) 72 | data = data.rechunk(chunks) 73 | 74 | # Save each frame 75 | for block in data.blocks: 76 | # Need to squeeze to remove the singleton T dimension 77 | writer.append_data(da.squeeze(block).compute()) 78 | 79 | @staticmethod 80 | def save( 81 | data: types.ArrayLike, 82 | uri: types.PathLike, 83 | dim_order: str = None, 84 | fps: int = 24, 85 | fs_kwargs: Dict[str, Any] = {}, 86 | **kwargs: Any, 87 | ) -> None: 88 | """ 89 | Write a data array to a file. 90 | 91 | Parameters 92 | ---------- 93 | data: types.ArrayLike 94 | The array of data to store. Data must have either three or four dimensions. 95 | uri: types.PathLike 96 | The URI or local path for where to save the data. 97 | dim_order: str 98 | The dimension order of the provided data. 99 | Default: None. Based off the number of dimensions, will assume 100 | the dimensions -- three dimensions: TYX and four dimensions: TYXS. 101 | fps: int 102 | Frames per second to attach as metadata. 103 | Default: 24 104 | fs_kwargs: Dict[str, Any] 105 | Any specific keyword arguments to pass down to the fsspec created 106 | filesystem. 107 | Default: {} 108 | 109 | Examples 110 | -------- 111 | Data is the correct shape and dimension order 112 | 113 | >>> image = dask.array.random.random((50, 100, 100)) 114 | ... TimeseriesWriter.save(image, "file.gif") 115 | 116 | Data provided with current dimension order 117 | 118 | >>> image = numpy.random.rand(100, 3, 1024, 2048) 119 | ... TimeseriesWriter.save(image, "file.mkv", "TSYX") 120 | 121 | Save to remote 122 | 123 | >>> image = numpy.random.rand(300, 100, 100, 3) 124 | ... TimeseriesWriter.save(image, "s3://my-bucket/file.png") 125 | 126 | Raises 127 | ------ 128 | IOError 129 | Cannot write FFMPEG formats to remote storage. 130 | 131 | Notes 132 | ----- 133 | This writer can also be useful when wanting to create a timeseries image using 134 | a non-time dimension. For example, creating a timeseries image where each frame 135 | is a Z-plane from a source volumetric image as seen below. 136 | 137 | >>> image = AICSImageIO("some_z_stack.ome.tiff") 138 | ... TimeseriesWriter.save( 139 | ... data=image.get_image_data("ZYX", T=0, C=0), 140 | ... uri="some_z_stack.mp4", 141 | ... # Overloading the Z dimension as the Time dimension 142 | ... # Technically not needed as it would have been assumed due to three dim 143 | ... dim_order="TYX", 144 | ... ) 145 | 146 | """ 147 | # Check unpack uri and extension 148 | fs, path = io_utils.pathlike_to_fs(uri, fs_kwargs=fs_kwargs) 149 | ( 150 | extension, 151 | imageio_mode, 152 | ) = DefaultReader._get_extension_and_mode(path) 153 | 154 | # Convert to dask array to make downstream usage of data have a consistent API 155 | if isinstance(data, np.ndarray): 156 | data = da.from_array(data) 157 | 158 | # Shorthand num dimensions 159 | n_dims = len(data.shape) 160 | 161 | # Check num dimensions 162 | if n_dims not in TimeseriesWriter.DIM_ORDERS: 163 | raise UnexpectedShapeError( 164 | f"TimeseriesWriter requires that data must have either 3 or 4 " 165 | f"dimensions. Provided data with {n_dims} dimensions. ({data.shape})" 166 | ) 167 | 168 | # Assume dim order if not provided 169 | if dim_order is None: 170 | dim_order = TimeseriesWriter.DIM_ORDERS[n_dims] 171 | 172 | # Uppercase dim order 173 | dim_order = dim_order.upper() 174 | 175 | # Check dimensions provided in the dim order string are all T, Y, X, or S 176 | if any( 177 | [ 178 | dim not in TimeseriesWriter._TIMEPOINT_WITH_SAMPLES_DIMENSIONS 179 | for dim in dim_order 180 | ] 181 | ): 182 | raise InvalidDimensionOrderingError( 183 | f"The dim_order parameter only accepts dimensions: " 184 | f"{TimeseriesWriter._TIMEPOINT_WITH_SAMPLES_DIMENSIONS}. " 185 | f"Provided dim_order string: '{dim_order}'." 186 | ) 187 | 188 | # Transpose dimensions if dim_order not ready for imageio 189 | if dim_order != TimeseriesWriter.DIM_ORDERS[n_dims]: 190 | # Actual reshape of the data 191 | data = reshape_data( 192 | data, 193 | given_dims=dim_order, 194 | return_dims=TimeseriesWriter.DIM_ORDERS[n_dims], 195 | ) 196 | 197 | # Set dim order to updated order 198 | dim_order = TimeseriesWriter.DIM_ORDERS[n_dims] 199 | 200 | # Handle FFMPEG formats 201 | if extension in DefaultReader.FFMPEG_FORMATS: 202 | # FFMPEG can only handle local files 203 | # https://github.com/imageio/imageio-ffmpeg/issues/28#issuecomment-566012783 204 | if not isinstance(fs, LocalFileSystem): 205 | raise IOError( 206 | f"Can only write to local files for formats: " 207 | f"{DefaultReader.FFMPEG_FORMATS}." 208 | ) 209 | 210 | # Else, write with local 211 | TimeseriesWriter._write_chunks( 212 | f=path, 213 | extension=extension, 214 | imageio_mode=imageio_mode, 215 | fps=fps, 216 | data=data, 217 | dim_order=dim_order, 218 | ) 219 | 220 | # Handle all non-ffmpeg formats 221 | else: 222 | with fs.open(path, "wb") as open_resource: 223 | TimeseriesWriter._write_chunks( 224 | f=open_resource, 225 | extension=extension, 226 | imageio_mode=imageio_mode, 227 | fps=fps, 228 | data=data, 229 | dim_order=dim_order, 230 | ) 231 | -------------------------------------------------------------------------------- /aicsimageio/writers/two_d_writer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from typing import Any, Dict 5 | 6 | import dask.array as da 7 | from imageio import get_writer 8 | 9 | from .. import types 10 | from ..dimensions import DimensionNames 11 | from ..exceptions import InvalidDimensionOrderingError, UnexpectedShapeError 12 | from ..transforms import reshape_data 13 | from ..utils import io_utils 14 | from .writer import Writer 15 | 16 | try: 17 | from ..readers.default_reader import DefaultReader 18 | 19 | except ImportError: 20 | raise ImportError( 21 | "Base imageio is required for this writer. " 22 | "Install with `pip install aicsimageio[base-imageio]`" 23 | ) 24 | 25 | ############################################################################### 26 | 27 | 28 | class TwoDWriter(Writer): 29 | """ 30 | A writer for image data is only 2 dimension with samples (RGB / RGBA) optional. 31 | Primarily directed at formats: "png", "jpg", etc. 32 | 33 | This is primarily a passthrough to imageio.imwrite. 34 | 35 | Notes 36 | ----- 37 | To use this writer, install with: `pip install aicsimageio[base-imageio]`. 38 | """ 39 | 40 | _PLANE_DIMENSIONS = [ 41 | DimensionNames.SpatialY, 42 | DimensionNames.SpatialX, 43 | ] 44 | _PLANE_WITH_SAMPLES_DIMENSIONS = _PLANE_DIMENSIONS + [DimensionNames.Samples] 45 | 46 | DIM_ORDERS = { 47 | 2: "".join(_PLANE_DIMENSIONS), # Greyscale 48 | 3: "".join(_PLANE_WITH_SAMPLES_DIMENSIONS), # RGB / RGBA 49 | } 50 | 51 | @staticmethod 52 | def save( 53 | data: types.ArrayLike, 54 | uri: types.PathLike, 55 | dim_order: str = None, 56 | fs_kwargs: Dict[str, Any] = {}, 57 | **kwargs: Any, 58 | ) -> None: 59 | """ 60 | Write a data array to a file. 61 | 62 | Parameters 63 | ---------- 64 | data: types.ArrayLike 65 | The array of data to store. Data must have either two or three dimensions. 66 | uri: types.PathLike 67 | The URI or local path for where to save the data. 68 | dim_order: str 69 | The dimension order of the provided data. 70 | Default: None. Based off the number of dimensions, will assume 71 | the dimensions similar to how 72 | aicsimageio.readers.default_reader.DefaultReader reads in 73 | data. That is, two dimensions: YX and three dimensions: YXS. 74 | fs_kwargs: Dict[str, Any] 75 | Any specific keyword arguments to pass down to the fsspec created 76 | filesystem. 77 | Default: {} 78 | 79 | Examples 80 | -------- 81 | Data is the correct shape and dimension order 82 | 83 | >>> image = dask.array.random.random((100, 100, 4)) 84 | ... TwoDWriter.save(image, "file.png") 85 | 86 | Data provided with current dimension order 87 | 88 | >>> image = numpy.random.rand(3, 1024, 2048) 89 | ... TwoDWriter.save(image, "file.png", "SYX") 90 | 91 | Save to remote 92 | 93 | >>> image = numpy.random.rand(100, 100, 3) 94 | ... TwoDWriter.save(image, "s3://my-bucket/file.png") 95 | """ 96 | # Check unpack uri and extension 97 | fs, path = io_utils.pathlike_to_fs(uri, fs_kwargs=fs_kwargs) 98 | ( 99 | extension, 100 | imageio_mode, 101 | ) = DefaultReader._get_extension_and_mode(path) 102 | 103 | # Assumption: if provided a dask array to save, it can fit into memory 104 | if isinstance(data, da.core.Array): 105 | data = data.compute() 106 | 107 | # Shorthand num dimensions 108 | n_dims = len(data.shape) 109 | 110 | # Check num dimensions 111 | if n_dims not in TwoDWriter.DIM_ORDERS: 112 | raise UnexpectedShapeError( 113 | f"TwoDWriter requires that data must have either 2 or 3 dimensions. " 114 | f"Provided data with {n_dims} dimensions. ({data.shape})" 115 | ) 116 | 117 | # Assume dim order if not provided 118 | if dim_order is None: 119 | dim_order = TwoDWriter.DIM_ORDERS[n_dims] 120 | 121 | # Uppercase dim order 122 | dim_order = dim_order.upper() 123 | 124 | # Check dimensions provided in the dim order string are all Y, X, or S 125 | if any( 126 | [dim not in TwoDWriter._PLANE_WITH_SAMPLES_DIMENSIONS for dim in dim_order] 127 | ): 128 | raise InvalidDimensionOrderingError( 129 | f"The dim_order parameter only accepts dimensions: " 130 | f"{TwoDWriter._PLANE_WITH_SAMPLES_DIMENSIONS}. " 131 | f"Provided dim_order string: '{dim_order}'." 132 | ) 133 | 134 | # Transpose dimensions if dim_order not ready for imageio 135 | if dim_order != TwoDWriter.DIM_ORDERS[n_dims]: 136 | data = reshape_data( 137 | data, given_dims=dim_order, return_dims=TwoDWriter.DIM_ORDERS[n_dims] 138 | ) 139 | 140 | # Save image 141 | with fs.open(path, "wb") as open_resource: 142 | with get_writer( 143 | open_resource, 144 | format=extension, 145 | mode=imageio_mode, 146 | ) as writer: 147 | writer.append_data(data) 148 | -------------------------------------------------------------------------------- /aicsimageio/writers/writer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from abc import ABC, abstractmethod 5 | from typing import Any 6 | 7 | from .. import types 8 | from ..dimensions import DEFAULT_DIMENSION_ORDER 9 | 10 | ############################################################################### 11 | 12 | 13 | class Writer(ABC): 14 | """ 15 | A small class to build standardized image writer functions. 16 | """ 17 | 18 | @staticmethod 19 | @abstractmethod 20 | def save( 21 | data: types.ArrayLike, 22 | uri: types.PathLike, 23 | dim_order: str = DEFAULT_DIMENSION_ORDER, 24 | **kwargs: Any 25 | ) -> None: 26 | """ 27 | Write a data array to a file. 28 | 29 | Parameters 30 | ---------- 31 | data: types.ArrayLike 32 | The array of data to store. 33 | uri: types.PathLike 34 | The URI or local path for where to save the data. 35 | dim_order: str 36 | The dimension order of the data. 37 | 38 | Examples 39 | -------- 40 | >>> image = numpy.ndarray([1, 10, 3, 1024, 2048]) 41 | ... DerivedWriter.save(image, "file.ome.tif", "TCZYX") 42 | 43 | >>> image = dask.array.ones((4, 100, 100)) 44 | ... DerivedWriter.save(image, "file.png", "CYX") 45 | """ 46 | # There are no requirements for n-dimensions of data. 47 | # The data provided can be 2D - ND. 48 | -------------------------------------------------------------------------------- /asv.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "project": "aicsimageio", 4 | "project_url": "http://AllenCellModeling.github.io/aicsimageio/", 5 | "repo": ".", 6 | "branches": ["main"], 7 | "dvcs": "git", 8 | "environment_type": "virtualenv", 9 | "install_command": [ 10 | "in-dir={env_dir} python -mpip install {build_dir}[benchmark]" 11 | ], 12 | "show_commit_url": "http://github.com/AllenCellModeling/aicsimageio/commit/", 13 | "pythons": ["3.10"], 14 | "benchmark_dir": "benchmarks", 15 | "env_dir": ".asv/env", 16 | "results_dir": ".asv/results", 17 | "html_dir": ".asv/html", 18 | "hash_length": 8, 19 | "build_cache_size": 8 20 | } 21 | -------------------------------------------------------------------------------- /benchmarks/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /benchmarks/benchmark_chunk_sizes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import dask.array as da 5 | import random 6 | from pathlib import Path 7 | 8 | from aicsimageio import AICSImage 9 | 10 | from .benchmark_image_containers import _ImageContainerTimeSuite 11 | 12 | ############################################################################### 13 | 14 | # We only benchmark against local files as remote files are covered by unit tests 15 | # and are generally slower than local but scale at a similar rate. 16 | LOCAL_RESOURCES_DIR = ( 17 | Path(__file__).parent.parent / "aicsimageio" / "tests" / "resources" 18 | ) 19 | 20 | ############################################################################### 21 | 22 | 23 | class ChunkSuite(_ImageContainerTimeSuite): 24 | # This suite measures the effect that changing the default chunk dims 25 | # has on the duration of various reads. 26 | # We would expect that processing speed can be optimized based off of the 27 | # dimensions of the file and what the user is trying to do with said file. 28 | # i.e. If the user wants to normalize each channel and make a max projection 29 | # through Z, then the default of 'ZYX' is preferred over just 'YX'. 30 | # During this suite we not only benchmark the above example but also 31 | # file reading under the various chunk configurations as a monitor 32 | # for general read performance. 33 | 34 | params = ( 35 | [ 36 | str(LOCAL_RESOURCES_DIR / "pre-variance-cfe.ome.tiff"), 37 | str(LOCAL_RESOURCES_DIR / "variance-cfe.ome.tiff"), 38 | ], 39 | # We don't go above chunking by three dims because it would be rare 40 | # to do so... if you can read four-plus dims in a single chunk why can't you 41 | # just read in the whole image at once. 42 | # We also use CYX here to show that chunking with the _wrong_ dimensions can 43 | # result in longer processing times. 44 | [ 45 | "YX", 46 | "ZYX", 47 | "CYX", 48 | ], 49 | ) 50 | 51 | def time_norm_and_project(self, img_path, chunk_dims): 52 | """ 53 | Benchmark how long a norm and project through Z takes 54 | under various chunk dims configurations. 55 | """ 56 | # Init image container 57 | r = self.ImageContainer(img_path, chunk_dims=chunk_dims) 58 | 59 | # Store all delayed projections 60 | projs = [] 61 | 62 | # Only run a random sample of two channels instead of all 63 | selected_channels = random.sample(r.channel_names, 2) 64 | for i, channel_name in enumerate(r.channel_names): 65 | if channel_name in selected_channels: 66 | # Select each channel 67 | data = r.get_image_dask_data("ZYX", C=i) 68 | 69 | # Get percentile norm by values 70 | min_px_val, max_px_val = da.percentile( 71 | data.flatten(), 72 | [50.0, 99.8], 73 | ).compute() 74 | 75 | # Norm 76 | normed = (data - min_px_val) / (max_px_val - min_px_val) 77 | 78 | # Clip any values outside of 0 and 1 79 | clipped = da.clip(normed, 0, 1) 80 | 81 | # Scale them between 0 and 255 82 | scaled = clipped * 255 83 | 84 | # Create max project 85 | projs.append(scaled.max(axis=0)) 86 | 87 | # Compute all projections 88 | projs = da.stack(projs) 89 | projs.compute() 90 | 91 | def setup(self, img_path, chunk_dims): 92 | random.seed(42) 93 | self.ImageContainer = AICSImage 94 | -------------------------------------------------------------------------------- /benchmarks/benchmark_image_containers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import random 5 | from pathlib import Path 6 | 7 | from aicsimageio import AICSImage, readers 8 | from aicsimageio.dimensions import DEFAULT_CHUNK_DIMS, DimensionNames 9 | 10 | ############################################################################### 11 | 12 | # We only benchmark against local files as remote files are covered by unit tests 13 | # and are generally slower than local but scale at a similar rate. 14 | LOCAL_RESOURCES_DIR = ( 15 | Path(__file__).parent.parent / "aicsimageio" / "tests" / "resources" 16 | ) 17 | 18 | ############################################################################### 19 | 20 | 21 | class _ImageContainerMemorySuite: 22 | def peakmem_init(self, img_path): 23 | """ 24 | Benchmark how much memory is used for just the initialized image container. 25 | """ 26 | return self.ImageContainer(img_path) 27 | 28 | def peakmem_delayed_array(self, img_path): 29 | """ 30 | Benchmark how much memory is used for the image container once the 31 | delayed dask array is constructed. 32 | 33 | Serves as a comparison against the init. 34 | Metadata should account for most of the memory difference. 35 | """ 36 | r = self.ImageContainer(img_path) 37 | r.dask_data 38 | return r 39 | 40 | def peakmem_cached_array(self, img_path): 41 | """ 42 | Benchmark how much memory is used for the whole image container once the 43 | current scene is read into memory. 44 | 45 | Serves as a comparison against the delayed construct and as a sanity check. 46 | Estimate: `r.data.size * r.data.itemsize` + some metadata and object overhead. 47 | """ 48 | r = self.ImageContainer(img_path) 49 | r.data 50 | return r 51 | 52 | 53 | class _ImageContainerTimeSuite: 54 | 55 | # These default chunk dimensions don't exist on every image container 56 | # so we have to define them here as well 57 | DEFAULT_CHUNK_DIMS = [ 58 | DimensionNames.SpatialZ, 59 | DimensionNames.SpatialY, 60 | DimensionNames.SpatialX, 61 | DimensionNames.Samples, 62 | ] 63 | 64 | def time_init(self, img_path, chunk_dims=None): 65 | """ 66 | Benchmark how long it takes to validate a file and finish general setup. 67 | """ 68 | if chunk_dims is None: 69 | chunk_dims = DEFAULT_CHUNK_DIMS 70 | 71 | self.ImageContainer(img_path, chunk_dims=chunk_dims) 72 | 73 | def time_delayed_array_construct(self, img_path, chunk_dims=None): 74 | """ 75 | Benchmark how long it takes to construct the delayed dask array for a file. 76 | """ 77 | if chunk_dims is None: 78 | chunk_dims = DEFAULT_CHUNK_DIMS 79 | 80 | self.ImageContainer(img_path, chunk_dims=chunk_dims).dask_data 81 | 82 | def time_random_single_chunk_read(self, img_path, chunk_dims=None): 83 | """ 84 | Benchmark how long it takes to read a single chunk out of a file. 85 | 86 | I.E. "Pull just the Brightfield channel z-stack. 87 | """ 88 | if chunk_dims is None: 89 | chunk_dims = DEFAULT_CHUNK_DIMS 90 | 91 | r = self.ImageContainer(img_path, chunk_dims=chunk_dims) 92 | 93 | random_index_selections = {} 94 | for dim, size in zip(r.dims.order, r.dims.shape): 95 | if dim not in self.DEFAULT_CHUNK_DIMS: 96 | random_index_selections[dim] = random.randint(0, size - 1) 97 | 98 | valid_dims_to_return = "".join( 99 | [d for d in r.dims.order if d in self.DEFAULT_CHUNK_DIMS] 100 | ) 101 | r.get_image_dask_data(valid_dims_to_return, **random_index_selections).compute() 102 | 103 | def time_random_many_chunk_read(self, img_path, chunk_dims=None): 104 | """ 105 | Open a file, get many chunks out of the file at once. 106 | 107 | I.E. "Pull the DNA and Nucleus channel z-stacks, for the middle 50% timepoints". 108 | """ 109 | if chunk_dims is None: 110 | chunk_dims = DEFAULT_CHUNK_DIMS 111 | 112 | r = self.ImageContainer(img_path, chunk_dims=chunk_dims) 113 | 114 | random_index_selections = {} 115 | for dim, size in zip(r.dims.order, r.dims.shape): 116 | if dim not in self.DEFAULT_CHUNK_DIMS: 117 | a = random.randint(0, size - 1) 118 | b = random.randint(0, size - 1) 119 | lower = min(a, b) 120 | upper = max(a, b) 121 | random_index_selections[dim] = slice(lower, upper, 1) 122 | 123 | r.get_image_dask_data(r.dims.order, **random_index_selections).compute() 124 | 125 | 126 | ############################################################################### 127 | # ImageContainer benchmarks 128 | 129 | 130 | class DefaultReaderSuite(_ImageContainerTimeSuite, _ImageContainerMemorySuite): 131 | params = [ 132 | # We can't check any of the ffmpeg formats because asv doesn't run 133 | # properly with spawned subprocesses and the ffmpeg formats all 134 | # passthrough the request to ffmpeg... 135 | # 136 | # Because of this, these benchmarks are largely sanity checks 137 | sorted( 138 | [ 139 | str(LOCAL_RESOURCES_DIR / "example.bmp"), 140 | str(LOCAL_RESOURCES_DIR / "example.jpg"), 141 | str(LOCAL_RESOURCES_DIR / "example.png"), 142 | ] 143 | ), 144 | ] 145 | 146 | def setup(self, img_path): 147 | random.seed(42) 148 | self.ImageContainer = readers.default_reader.DefaultReader 149 | 150 | 151 | class TiffReaderSuite(_ImageContainerTimeSuite, _ImageContainerMemorySuite): 152 | params = [ 153 | [ 154 | str( 155 | LOCAL_RESOURCES_DIR 156 | / "image_stack_tpzc_50tp_2p_5z_3c_512k_1_MMStack_2-Pos001_000.ome.tif" 157 | ), 158 | str(LOCAL_RESOURCES_DIR / "variance-cfe.ome.tiff"), 159 | ] 160 | ] 161 | 162 | def setup(self, img_path): 163 | random.seed(42) 164 | self.ImageContainer = readers.tiff_reader.TiffReader 165 | 166 | 167 | class OmeTiffReaderSuite(_ImageContainerTimeSuite, _ImageContainerMemorySuite): 168 | params = [ 169 | [ 170 | str(LOCAL_RESOURCES_DIR / "actk.ome.tiff"), 171 | str(LOCAL_RESOURCES_DIR / "pre-variance-cfe.ome.tiff"), 172 | str(LOCAL_RESOURCES_DIR / "variance-cfe.ome.tiff"), 173 | ] 174 | ] 175 | 176 | def setup(self, img_path): 177 | random.seed(42) 178 | self.ImageContainer = readers.ome_tiff_reader.OmeTiffReader 179 | 180 | 181 | class OmeTiledTiffReaderSuite(_ImageContainerTimeSuite, _ImageContainerMemorySuite): 182 | params = [ 183 | [ 184 | str(LOCAL_RESOURCES_DIR / "actk_ome_tiff_tiles.ome.tif"), 185 | str(LOCAL_RESOURCES_DIR / "pre-variance-cfe_ome_tiff_tiles.ome.tif"), 186 | str(LOCAL_RESOURCES_DIR / "variance-cfe_ome_tiff_tiles.ome.tif"), 187 | ] 188 | ] 189 | 190 | def setup(self, img_path): 191 | random.seed(42) 192 | self.ImageContainer = readers.bfio_reader.OmeTiledTiffReader 193 | 194 | 195 | class LifReaderSuite(_ImageContainerTimeSuite, _ImageContainerMemorySuite): 196 | params = [ 197 | sorted([str(f) for f in LOCAL_RESOURCES_DIR.glob("*.lif")]), 198 | ] 199 | 200 | def setup(self, img_path): 201 | random.seed(42) 202 | self.ImageContainer = readers.lif_reader.LifReader 203 | 204 | 205 | class CziReaderSuite(_ImageContainerTimeSuite, _ImageContainerMemorySuite): 206 | params = [ 207 | sorted([str(f) for f in LOCAL_RESOURCES_DIR.glob("*.czi")]), 208 | ] 209 | 210 | def setup(self, img_path): 211 | random.seed(42) 212 | self.ImageContainer = readers.czi_reader.CziReader 213 | 214 | 215 | class AICSImageSuite(_ImageContainerTimeSuite, _ImageContainerMemorySuite): 216 | # This suite utilizes the same suite that the base readers do. 217 | # In all cases, the time or peak memory used by AICSImage should 218 | # be minimal additional overhead from the base reader. 219 | 220 | params = list( 221 | set( 222 | DefaultReaderSuite.params[0] 223 | + TiffReaderSuite.params[0] 224 | + OmeTiffReaderSuite.params[0] 225 | + OmeTiledTiffReaderSuite.params[0] 226 | + LifReaderSuite.params[0] 227 | + CziReaderSuite.params[0] 228 | ) 229 | ) 230 | 231 | def setup(self, img_path): 232 | random.seed(42) 233 | self.ImageContainer = AICSImage 234 | -------------------------------------------------------------------------------- /benchmarks/benchmark_lib.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Benchmarks for general library operations and comparisons against other libraries. 6 | """ 7 | 8 | from functools import partial 9 | 10 | from aicsimageio import imread_dask as aicsimageio_imread 11 | from dask_image.imread import imread as dask_image_imread 12 | 13 | from .benchmark_image_containers import LOCAL_RESOURCES_DIR 14 | 15 | ############################################################################### 16 | 17 | ACTK_OME_TIFF = str(LOCAL_RESOURCES_DIR / "actk.ome.tiff") 18 | 19 | ############################################################################### 20 | 21 | 22 | class LibInitSuite: 23 | def time_base_import(self): 24 | """ 25 | Benchmark how long it takes to import the library as a whole. 26 | """ 27 | import aicsimageio # noqa: F401 28 | 29 | 30 | class LibCompareSuite: 31 | """ 32 | Compare aicsimageio against other "just-in-time" image reading libs. 33 | """ 34 | 35 | FUNC_LOOKUP = { 36 | "aicsimageio-default-chunks": partial(aicsimageio_imread, chunk_dims="ZYX"), 37 | "aicsimageio-plane-chunks": partial(aicsimageio_imread, chunk_dims="YX"), 38 | "dask-image-imread-default": dask_image_imread, 39 | } 40 | 41 | params = [ 42 | "aicsimageio-default-chunks", 43 | "aicsimageio-plane-chunks", 44 | "dask-image-imread-default", 45 | ] 46 | 47 | def time_lib_config(self, func_name): 48 | func = self.FUNC_LOOKUP[func_name] 49 | func(ACTK_OME_TIFF).compute() 50 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "aicsimageio/vendor/.*" 3 | - "**/__init__.py" 4 | 5 | coverage: 6 | status: 7 | patch: off 8 | -------------------------------------------------------------------------------- /cookiecutter.yaml: -------------------------------------------------------------------------------- 1 | default_context: 2 | full_name: "Eva Maxfield Brown, Allen Institute for Cell Science" 3 | email: "evamaxfieldbrown@gmail.com, jamie.sherman@gmail.com, bowdenm@spu.edu" 4 | github_username: "AllenCellModeling" 5 | project_name: "AICSImageIO" 6 | project_slug: "aicsimageio" 7 | project_short_description: "Image Reading, Metadata Conversion, and Image Writing for Microscopy Images in Pure Python" 8 | pypi_username: "aicspypi" 9 | version: "4.14.0" 10 | open_source_license: "BSD license" -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting any of the contributors from the Allen Institute and 59 | we will attempt to resolve the issues with respect and dignity. 60 | 61 | Project maintainers who do not follow or enforce the Code of Conduct in good 62 | faith may face temporary or permanent repercussions as determined by other 63 | members of the project's leadership. 64 | 65 | ## Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 68 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 69 | 70 | [homepage]: https://www.contributor-covenant.org 71 | 72 | For answers to common questions about this code of conduct, see 73 | https://www.contributor-covenant.org/faq 74 | -------------------------------------------------------------------------------- /docs/GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # Governance Model 2 | 3 | ## Abstract 4 | 5 | The purpose of this document is to formalize the governance process used by the 6 | `aicsimageio` project, to clarify how decisions are made and how the various 7 | elements of our community interact. 8 | 9 | This is a consensus-based community project. Anyone with an interest in the 10 | project can join the community, contribute to the project design, and 11 | participate in the decision making process. This document describes how that 12 | participation takes place, how to find consensus, and how deadlocks are 13 | resolved. 14 | 15 | ## Roles And Responsibilities 16 | 17 | ### The Community 18 | 19 | The aicsimageio community consists of anyone using or working with the project 20 | in any way. 21 | 22 | ### Contributors 23 | 24 | A community member can become a contributor by interacting directly with the 25 | project in concrete ways, such as: 26 | 27 | - proposing a change to the code via a 28 | [GitHub pull request](https://github.com/AllenCellModeling/aicsimageio/pulls); 29 | - reporting issues on our 30 | [GitHub issues page](https://github.com/AllenCellModeling/aicsimageio/issues); 31 | - proposing a change to the documentation via a 32 | [GitHub pull request](https://github.com/AllenCellModeling/aicsimageio/pulls); 33 | - discussing the design of aicsimageio on existing 34 | [issues](https://github.com/AllenCellModeling/aicsimageio/issues) and / or 35 | [pull requests](https://github.com/AllenCellModeling/aicsimageio/pulls); 36 | - reviewing [open pull requests](https://github.com/AllenCellModeling/aicsimageio/pulls) 37 | 38 | among other possibilities. Any community member can become a contributor, and 39 | all are encouraged to do so. By contributing to the project, community members 40 | can directly help to shape its future. 41 | 42 | Contributors are encouraged to read the [contributing guide](./CONTRIBUTING.md). 43 | 44 | ### Core developers 45 | 46 | Core developers are community members that have demonstrated continued 47 | commitment to the project through ongoing contributions. They 48 | have shown they can be trusted to maintain aicsimageio with care. Becoming a 49 | core developer allows contributors to merge approved pull requests, cast votes 50 | for and against merging a pull-request, and be involved in deciding major 51 | changes to the API, and thereby more easily carry on with their project related 52 | activities. Core developers appear as team members on our 53 | [@AllenCellModeling/aicsimageio-core-devs](https://github.com/orgs/AllenCellModeling/teams/aicsimageio-core-devs/members) 54 | GitHub team. Core developers are asked to review code contributions. New core 55 | developers can be nominated by any existing core developer. 56 | 57 | ### Steering Council 58 | 59 | The Steering Council (SC) members are core developers who have additional 60 | responsibilities to ensure the smooth running of the project. SC members are 61 | expected to participate in strategic planning, approve changes to the 62 | governance model. The purpose of the SC is to ensure smooth progress from the big 63 | picture perspective. Changes that impact the full project require analysis informed by 64 | long experience with both the project and the larger ecosystem. When the core 65 | developer community (including the SC members) fails to reach such a consensus 66 | in a reasonable time-frame, the SC is the entity that resolves the issue. 67 | 68 | The steering council is currently fixed to only include members directly from Allen 69 | Institute for Cell Science. This may be changed in the future, but this results in the 70 | steering council currently consisting of: 71 | 72 | - [Eva Maxfield Brown](https://github.com/evamaxfield) 73 | - [Daniel Toloudis](https://github.com/toloudis) 74 | 75 | New members are added by nomination by a core developer. Nominees should have 76 | demonstrated long-term, continued commitment to the project and its 77 | [mission and values](./MISSION_AND_VALUES.md). 78 | 79 | ## Decision Making Process 80 | 81 | Decisions about the future of the project are made through discussion with all 82 | members of the community. All non-sensitive project management discussion takes 83 | place on the [issue tracker](https://github.com/AllenCellModeling/aicsimageio/issues). 84 | Occasionally, sensitive discussion may occur through a private core developer channel. 85 | 86 | Decisions should be made in accordance with the 87 | [mission and values](./MISSION_AND_VALUES.md) of the aicsimageio project. 88 | 89 | aicsimageio uses a “consensus seeking” process for making decisions. The group 90 | tries to find a resolution that has no open objections among core developers. 91 | Core developers are expected to distinguish between fundamental objections to a 92 | proposal and minor perceived flaws that they can live with, and not hold up the 93 | decision-making process for the latter. 94 | -------------------------------------------------------------------------------- /docs/INSTALLATION.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | Stable release 9 | -------------- 10 | 11 | To install aicsimageio, run this command in your terminal: 12 | 13 | .. code-block:: console 14 | 15 | $ pip install aicsimageio 16 | 17 | This is the preferred method to install aicsimageio, as it will always install the most recent stable release. 18 | 19 | If you don't have `pip`_ installed, this `Python installation guide`_ can guide 20 | you through the process. 21 | 22 | .. _pip: https://pip.pypa.io 23 | .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ 24 | 25 | 26 | From sources 27 | ------------ 28 | 29 | The sources for aicsimageio can be downloaded from the `Github repo`_. 30 | 31 | You can either clone the public repository and submodules: 32 | 33 | .. code-block:: console 34 | 35 | $ git clone git://github.com/AllenCellModeling/aicsimageio 36 | $ git submodule update --init --recursive 37 | 38 | Or download the `tarball`_: 39 | 40 | .. code-block:: console 41 | 42 | $ curl -OL https://github.com/AllenCellModeling/aicsimageio/tarball/main 43 | 44 | Once you have a copy of the source, you can install it with: 45 | 46 | .. code-block:: console 47 | 48 | $ python setup.py install 49 | 50 | 51 | .. _Github repo: https://github.com/AllenCellModeling/aicsimageio 52 | .. _tarball: https://github.com/AllenCellModeling/aicsimageio/tarball/main 53 | -------------------------------------------------------------------------------- /docs/MISSION_AND_VALUES.md: -------------------------------------------------------------------------------- 1 | # Mission and Values 2 | 3 | This document is meant to help guide decisions about the future of AICSImageIO, be it 4 | in terms of whether to accept new functionality, changes to existing functionality, 5 | changes to package administrative tasks, etc. It serves as a reference for core 6 | developers in guiding their work, and, as an introduction for newcomers who want to 7 | learn where the project is headed and what the team's values are. You can also learn 8 | how the project is managed by looking at our [governance model](./GOVERNANCE.md). 9 | 10 | ## Mission 11 | 12 | AICSImageIO aims to provide a **consistent intuitive API for reading in or out-of-memory 13 | image pixel data and metadata** for the many existing proprietary microscopy file 14 | formats, and, an **easy-to-use API for converting from proprietary file formats to an 15 | open, common, standard** -- all using either language agnostic or pure Python tooling. 16 | 17 | In short: 18 | > AICSImageIO provides a method to fully read and convert from an existing proprietary 19 | > microscopy file format to, or _emulate_, a Python representation of the community 20 | > standard metadata model regardless of image size, format, or location. 21 | 22 | (The current community standard for microscopy images is the 23 | [Open Microscopy Environment](https://www.openmicroscopy.org/)) 24 | 25 | We hope to accomplish this by: 26 | * being **easy to use and install**. We will take extra care to ensure that this library 27 | is easy to use and fully installable on Windows, Mac-OS, and Ubuntu. 28 | * being **well-documented** with our entire API having up-to-date, useful docstrings 29 | and additionally providing examples of more complex use-cases when possible. 30 | * providing a **consistent and stable API** to users by following 31 | [semantic versioning](https://semver.org/) and limiting the amount of breaking changes 32 | introduced unless necessary for the future robustness or scalability of the library. 33 | * sustaining **comparable or better performance when compared to more tailored file 34 | format reading libraries**. We will regularly run benchmarks utilizing a set of varied 35 | size images from all the file formats the library is capable of reading. 36 | * **working closely with the microscopy community** while deciding on standards and best 37 | practices for open, accessible, file formats and imaging and in deciding which 38 | proprietary file formats and metadata selection are in need of support. 39 | 40 | ## Values 41 | * We are **inclusive**. We welcome and mentor newcomers who are making their first 42 | contribution and strive to grow our most dedicated contributors into core developers. We 43 | have a [Code of Conduct](./CODE_OF_CONDUCT.md) to ensure that the AICSImageIO remains 44 | a welcoming place for all. 45 | * We are **community-driven**. We respond to feature requests and proposals on our 46 | [issue tracker](https://github.com/AllenCellModeling/aicsimageio/issues) and make 47 | decisions that are driven by our user's requirements. 48 | * We focus on **file IO and metadata conversion**, leaving image analysis functionality 49 | and visualization to other libraries. 50 | * We aim to **develop new methods of metadata extraction and conversion**, instead of 51 | duplicating existing, or porting, from other libraries primarily by creating **language 52 | agnostic methods** for metadata manipulation. 53 | * We value **simple, readable implementations**. Readable code that is easy to 54 | understand, for newcomers and maintainers alike, makes it easier to contribute new code 55 | as well as prevent bugs. 56 | * We value **education and documentation**. All functions should have docstrings, 57 | preferably with examples, and major functionality should be explained in our tutorials. 58 | Core developers can take an active role in finishing documentation examples. 59 | * We **minimize [magic](https://en.wikipedia.org/wiki/Magic_(programming))** and always 60 | provide a way for users to opt out of magical behaviour and guessing by providing 61 | explicit ways to control functionality. 62 | 63 | ## Acknowledgments 64 | We share a lot of our mission and values with the `napari` project, and acknowledge the 65 | influence of their mission and values statement on this document. 66 | 67 | Additionally, much of the work produced for this library is built on the shoulders of 68 | giants. Notably: 69 | * [Christoph Gohlke](https://www.lfd.uci.edu/~gohlke/) -- maintainer of `tifffile`, 70 | `czifile`, and the `imagecodecs` libraries 71 | * [Paul Watkins](https://github.com/elhuhdron) -- original creator of `pylibczi` 72 | * [OME and Bio-Formats Team](https://github.com/ome/bioformats) -- proprietary 73 | microscopy file format conversion and standards development 74 | * [Python-Bio-Formats Team](https://github.com/CellProfiler/python-bioformats) -- 75 | Python Java Bridge for Bio-Formats and original implementations of OME Python 76 | representation 77 | * [imageio Team](https://github.com/imageio/imageio) -- robust, expansive, cross 78 | platform image reading 79 | * [Dask Team](https://dask.org/) -- delayed and out-of-memory parallel array 80 | manipulation 81 | * [xarray Team](https://github.com/pydata/xarray) -- coordinate and metadata attached 82 | array handling and manipulation 83 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = aicsimageio 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/ROADMAP.md: -------------------------------------------------------------------------------- 1 | # AICSImageIO Roadmap 2 | 3 | ## For 4.* Series of Releases - September 2020 4 | The `aicsimageio` roadmap captures current development priorities within the project 5 | and should serve as a guide for core developers, to encourage contribution for new 6 | contributors, and to provide insight to external developers who are interested in using 7 | `aicsimageio` in their work. 8 | 9 | The [mission](./MISSION_AND_VALUES.md) of `aicsimageio` is to provide a method to 10 | fully read and convert from an existing proprietary microscopy file format to, or 11 | _emulate_, a Python representation of the community standard metadata model regardless 12 | of image size, format, or location. To work towards this mission, we have set a few 13 | high-level priorities over the upcoming months: 14 | 15 | * Enable **reading and writing to remote sources** quick and easy 16 | * Make **writing n-dimensional images** with partial or complete metadata **simple** 17 | * Make **converting and accessing metadata** as easy as possible with a **standard API** 18 | * Expand the list of currently available **proprietary file format readers** 19 | 20 | ## Enable Reading and Writing to Remote Sources Quick and Easy 21 | While most microscopy labs and computational scientists currently work with entirely 22 | local datasets, it is becoming increasingly common for work to be conducted entirely on 23 | the cloud. To address this, we plan to allow remote paths or pointers to data directly 24 | into the high-level functions and objects of this library. 25 | 26 | Fortunately, many existing frameworks and libraries can handle this for us to a large 27 | extent but do come with some concerns about how to do so efficiently and easily, 28 | specifically on the chunked / out-of-memory reading side. 29 | 30 | In-memory reading from remote sources is relatively simple for all formats. Out-of 31 | memory reading for formats not-initially designed to handled remote friendly chunked 32 | reading are the crux of the issue (i.e. TIFF, CZI, LIF, etc.). Adding remote reading is 33 | great for an initial 4.0 addition but _optimizing_ chunked remote reading wherever 34 | possible in future work of the 4.* series is a high priority. 35 | 36 | Our [benchmarks](./BENCHMARKS.md) will be incredibly useful in tracking and 37 | maintaining our performance. 38 | 39 | ## Make Writing n-dimensional Images with Partial or Complete Metadata Simple 40 | N-dimensional image writing _with_ metadata has largely been ignored outside of the work 41 | done by the proprietary file format developers themselves. To do so for the general 42 | computational scientist requires much more flexibility in what to allow as valid 43 | metadata, and, the bare minimum needed to deem a file as having "useful" metadata. 44 | 45 | "Useful" is largely defined by the community so we will not define it ourselves, but, 46 | looking to the community and answering the call for a simple n-dimensional image writer 47 | _with "useful" metadata attachment_ is where we can contribute. 48 | 49 | In general, we have followed the [OME](https://www.openmicroscopy.org/) team in 50 | metadata specification and have participated in the OME Community Meeting's in 51 | discussing the exact question of "what is 'useful' metadata?" Keeping with them, we will 52 | continue to improve the OME-TIFF writing experience for both _updating_ image metadata 53 | or generating bare minimum metadata for a research / processing result. 54 | 55 | In addition, addressing the problems with reading and writing _remote_ data mentioned 56 | previously, we plan to add writing to a remote source as baseline functionality, making 57 | it efficient, and adding more file format writers to the library as more file formats 58 | better equipped to handle chunked remote reading become available, i.e. 59 | [Zarr](https://zarr.readthedocs.io/en/stable/) and OME-Zarr. 60 | 61 | ## Make Converting and Accessing Metadata as Easy as Possible with a Standard API 62 | As more and more proprietary file format readers are added to the library all with their 63 | own metadata schema, it becomes harder and harder for computational scientists to 64 | quickly switch from one dataset to another simply based off of their dataset's file 65 | format. Additionally, we recognize the work already done by the 66 | [OME](https://www.openmicroscopy.org/) and 67 | [Python Bio-Formats](https://github.com/CellProfiler/python-bioformats) teams in making 68 | this even remotely possible to begin with. We want to expand on their existing work and 69 | address the problems of scaling to multiple programming languages, making it easier for 70 | non-computational users to contribute, and more. 71 | 72 | To address this we are planning a large chunk of work around "language-agnostic metadata 73 | schema conversion." We have already begun to prototype this work with 74 | [CZI-to-OME-XSLT](https://github.com/allencellmodeling/czi-to-ome-xslt), a repository 75 | that can be used in any language that has [XSLT](https://en.wikipedia.org/wiki/XSLT) 76 | support or a usage library. 77 | 78 | For us this would mean two things: 79 | 1. a more generalized form of metadata schema conversion that multiple languages can use 80 | and contribute back to instead of duplicated work in many languages 81 | 2. a simple system to convert metadata schema, allowing for a unified access API 82 | 83 | While we will not make it a requirement that with the addition of a new proprietary 84 | file format reader to the library, the contributor must also add a sub-module to a 85 | metadata schema conversion repository, we will however _highly encourage it._ We 86 | believe that this strategy will allow us in the long-run a more maintainable code base 87 | as well as a simple end-user API because, in general, metadata conversion is cheap and 88 | fast, allowing us to convert to a common standard (OME) on request and handle all 89 | metadata queries using the same schema. 90 | 91 | In the case where no conversion XSLT or similar "language-agnostic method" sub-module 92 | is added, we will still make an effort to extract the metadata in code while still 93 | conforming to the standard API for the time being. 94 | 95 | ## Expand the List of Currently Available Proprietary File Format Readers 96 | Looking to our needs at the Allen Institute for Cell Science first, we will continue to 97 | add new readers to the library as needed. However, we will happily accept, review, and 98 | help maintain new proprietary file format readers to the library from contributors. 99 | 100 | With a few exceptions, in general the only requirement of a new file format to be 101 | supported would be that the reader must be able to accept local and remote requests as 102 | per our previously stated goals. 103 | 104 | Specifically for the Allen Institute for Cell Science, we would like to add readers for: 105 | * Slidebook (`.sld`) 106 | * Zarr (`.zarr`) 107 | * OME-Zarr (`.ome.zarr`) 108 | 109 | Our greatest hope would be that the proprietary file format developer themselves added 110 | new readers, and, as previously described, metadata conversion functionality. 111 | Historically however, this has not been the case. We will work on fostering 112 | relationships with the developers of the priority file formats where possible to 113 | change this, whether it is for `aicsimageio` or another library -- open access and open 114 | standards are still better, regardless of `aicsimageio` or not. 115 | 116 | ## About This Document 117 | This document is meant to be a snapshot or high-level objectives and reasoning for the 118 | library during our 4.* series of releases starting in September 2020. 119 | 120 | For more low-level implementation details, features, bugs, documentation requests, etc, 121 | please see our [issue tracker](https://github.com/AllenCellModeling/aicsimageio/issues). 122 | -------------------------------------------------------------------------------- /docs/_fix_internal_links.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Fixes internal cross-documentation links by switching out their 6 | suffixes. I.E. we want the docs to work both as normal files on 7 | GitHub (using markdown), and, when converted and rendered as HTML. 8 | """ 9 | 10 | 11 | import argparse 12 | import logging 13 | import re 14 | from pathlib import Path 15 | 16 | ############################################################################### 17 | 18 | logging.basicConfig( 19 | level=logging.INFO, 20 | format="[%(levelname)4s: %(module)s:%(lineno)4s %(asctime)s] %(message)s", 21 | ) 22 | log = logging.getLogger(__name__) 23 | 24 | ############################################################################### 25 | 26 | 27 | class Args(argparse.Namespace): 28 | def __init__(self): 29 | self.__parse() 30 | 31 | def __parse(self): 32 | # Setup parser 33 | p = argparse.ArgumentParser( 34 | prog="fix_internal_links", 35 | description=( 36 | "Fixes internal cross-documentation links by switching out their " 37 | "suffixes. I.E. we want the docs to work both as normal files on " 38 | "GitHub (using markdown), and, when converted and rendered as HTML." 39 | ), 40 | ) 41 | 42 | # Arguments 43 | p.add_argument( 44 | "--current-suffix", 45 | default=".md", 46 | dest="current_suffix", 47 | help="The suffix to switch internal cross-documentation references from.", 48 | ) 49 | p.add_argument( 50 | "--target-suffix", 51 | default=".html", 52 | dest="target_suffix", 53 | help="The suffix to switch internal cross-documentation references to.", 54 | ) 55 | 56 | # Parse 57 | p.parse_args(namespace=self) 58 | 59 | 60 | ############################################################################### 61 | 62 | 63 | def fix_file(f: Path, current_suffix: str = ".md", target_suffix: str = ".html"): 64 | # We could use formatted strings here but {} are valid characters in regex 65 | # instead just use string appending 66 | # 67 | # Look for exact characters "./" followed by at least one, upper or lower A-Z 68 | # and allow hyphen and underscore characters 69 | # followed by the dev suffix 70 | # Group 1 is the file_name 71 | # Group 2 is the suffix 72 | RE_SUB_CURRENT = r"(\.\/[a-zA-Z_-]+)(" + current_suffix + r")" 73 | 74 | # Keep group 1 75 | # attach the new suffix 76 | RE_SUB_TARGET = r"\1" + target_suffix 77 | 78 | # Read in text 79 | with open(f, "r") as open_resource: 80 | txt = open_resource.read() 81 | 82 | # Fix suffixes 83 | cleaned = re.sub(RE_SUB_CURRENT, RE_SUB_TARGET, txt) 84 | 85 | with open(f, "w") as open_resource: 86 | open_resource.write(cleaned) 87 | 88 | 89 | ############################################################################### 90 | 91 | 92 | def main(): 93 | args = Args() 94 | 95 | # Get docs dir 96 | docs = Path(__file__).parent.resolve() 97 | 98 | # Get files in dir 99 | for f in docs.glob("*.md"): 100 | fix_file(f, args.current_suffix, args.target_suffix) 101 | log.info(f"Cleaned file: {f}") 102 | 103 | 104 | ############################################################################### 105 | # Allow caller to directly run this module (usually in development scenarios) 106 | 107 | if __name__ == "__main__": 108 | main() 109 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # aicsimageio documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Jun 9 13:47:02 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another 17 | # directory, add these directories to sys.path here. If the directory is 18 | # relative to the documentation root, use os.path.abspath to make it 19 | # absolute, like shown here. 20 | # 21 | import os 22 | import sys 23 | 24 | import sphinx_rtd_theme 25 | 26 | import aicsimageio 27 | 28 | sys.path.insert(0, os.path.abspath("..")) 29 | 30 | 31 | # -- General configuration --------------------------------------------- 32 | 33 | # If your documentation needs a minimal Sphinx version, state it here. 34 | # 35 | # needs_sphinx = "1.0" 36 | 37 | # Add any Sphinx extension module names here, as strings. They can be 38 | # extensions coming with Sphinx (named "sphinx.ext.*") or your custom ones. 39 | extensions = [ 40 | "sphinx.ext.autodoc", 41 | "sphinx.ext.autosummary", 42 | "sphinx.ext.viewcode", 43 | "sphinx.ext.napoleon", 44 | "sphinx.ext.mathjax", 45 | "m2r2", 46 | ] 47 | 48 | # Control napoleon 49 | napoleon_google_docstring = False 50 | napolean_include_init_with_doc = True 51 | napoleon_use_ivar = True 52 | napoleon_use_param = False 53 | 54 | # Control autodoc 55 | autoclass_content = "both" # include init doc with class 56 | 57 | # Add any paths that contain templates here, relative to this directory. 58 | templates_path = ["_templates"] 59 | 60 | # The suffix(es) of source filenames. 61 | # You can specify multiple suffix as a list of string: 62 | # 63 | source_suffix = { 64 | ".rst": "restructuredtext", 65 | ".txt": "markdown", 66 | ".md": "markdown", 67 | } 68 | 69 | # The main toctree document. 70 | main_doc = "index" 71 | 72 | # General information about the project. 73 | project = "AICSImageIO" 74 | copyright = "2022, Eva Maxfield Brown, Allen Institute for Cell Science" 75 | author = "Eva Maxfield Brown, Allen Institute for Cell Science" 76 | 77 | # The version info for the project you"re documenting, acts as replacement 78 | # for |version| and |release|, also used in various other places throughout 79 | # the built documents. 80 | # 81 | # The short X.Y version. 82 | version = aicsimageio.__version__ 83 | # The full version, including alpha/beta/rc tags. 84 | release = aicsimageio.__version__ 85 | 86 | # The language for content autogenerated by Sphinx. Refer to documentation 87 | # for a list of supported languages. 88 | # 89 | # This is also used if you do content translation via gettext catalogs. 90 | # Usually you set "language" from the command line for these cases. 91 | language = None 92 | 93 | # List of patterns, relative to source directory, that match files and 94 | # directories to ignore when looking for source files. 95 | # This patterns also effect to html_static_path and html_extra_path 96 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 97 | 98 | # The name of the Pygments (syntax highlighting) style to use. 99 | pygments_style = "sphinx" 100 | 101 | # If true, `todo` and `todoList` produce output, else they produce nothing. 102 | todo_include_todos = False 103 | 104 | 105 | # -- Options for HTML output ------------------------------------------- 106 | 107 | # The theme to use for HTML and HTML Help pages. See the documentation for 108 | # a list of builtin themes. 109 | # 110 | html_theme = "sphinx_rtd_theme" 111 | 112 | # Theme options are theme-specific and customize the look and feel of a 113 | # theme further. For a list of options available for each theme, see the 114 | # documentation. 115 | # 116 | html_theme_options = { 117 | "collapse_navigation": False, 118 | "prev_next_buttons_location": "top", 119 | } 120 | 121 | # Add any paths that contain custom static files (such as style sheets) here, 122 | # relative to this directory. They are copied after the builtin static files, 123 | # so a file named "default.css" will overwrite the builtin "default.css". 124 | html_static_path = ["_static"] 125 | 126 | 127 | # -- Options for HTMLHelp output --------------------------------------- 128 | 129 | # Output file base name for HTML help builder. 130 | htmlhelp_basename = "aicsimageiodoc" 131 | 132 | 133 | # -- Options for LaTeX output ------------------------------------------ 134 | 135 | latex_elements = { 136 | # The paper size ("letterpaper" or "a4paper"). 137 | # 138 | # "papersize": "letterpaper", 139 | # The font size ("10pt", "11pt" or "12pt"). 140 | # 141 | # "pointsize": "10pt", 142 | # Additional stuff for the LaTeX preamble. 143 | # 144 | # "preamble": "", 145 | # Latex figure (float) alignment 146 | # 147 | # "figure_align": "htbp", 148 | } 149 | 150 | # Grouping the document tree into LaTeX files. List of tuples 151 | # (source start file, target name, title, author, documentclass 152 | # [howto, manual, or own class]). 153 | latex_documents = [ 154 | ( 155 | main_doc, 156 | "aicsimageio.tex", 157 | "AICSImageIO Documentation", 158 | "Eva Maxfield Brown, Allen Institute for Cell Science", 159 | "manual", 160 | ), 161 | ] 162 | 163 | 164 | # -- Options for manual page output ------------------------------------ 165 | 166 | # One entry per manual page. List of tuples 167 | # (source start file, name, description, authors, manual section). 168 | man_pages = [(main_doc, "aicsimageio", "AICSImageIO Documentation", [author], 1)] 169 | 170 | 171 | # -- Options for Texinfo output ---------------------------------------- 172 | 173 | # Grouping the document tree into Texinfo files. List of tuples 174 | # (source start file, target name, title, author, 175 | # dir menu entry, description, category) 176 | texinfo_documents = [ 177 | ( 178 | main_doc, 179 | "aicsimageio", 180 | "AICSImageIO Documentation", 181 | author, 182 | "aicsimageio", 183 | ( 184 | "Image Reading, Metadata Conversion, and Image Writing for Microscopy " 185 | "Images in Pure Python" 186 | ), 187 | ), 188 | ] 189 | -------------------------------------------------------------------------------- /docs/developer_resources.rst: -------------------------------------------------------------------------------- 1 | Developer Resources 2 | =================== 3 | 4 | A compiled list of resources for new and existing developers and maintainers of the 5 | library. If you believe a document or information is missing please check our 6 | `issue tracker `_. 7 | 8 | .. toctree:: 9 | :hidden: 10 | :maxdepth: 1 11 | :caption: Contents: 12 | 13 | Benchmarks <./_benchmarks/index.html#http://> 14 | Contributing 15 | Code of Conduct 16 | Governance Model 17 | Mission and Values 18 | Roadmap 19 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. toctree:: 2 | :hidden: 3 | :maxdepth: 1 4 | :caption: Contents: 5 | 6 | Overview 7 | Installation 8 | Full API Reference 9 | Developer Resources 10 | Changelog 11 | 12 | .. mdinclude:: ../README.md 13 | 14 | Specific Doc Pages 15 | ================== 16 | 17 | .. autosummary:: 18 | :toctree: 19 | :caption: Important Classes: 20 | 21 | aicsimageio.aics_image.AICSImage 22 | aicsimageio.readers.reader.Reader 23 | aicsimageio.readers.ome_tiff_reader.OmeTiffReader 24 | aicsimageio.readers.tiff_reader.TiffReader 25 | aicsimageio.writers.ome_tiff_writer.OmeTiffWriter 26 | 27 | Indices and tables 28 | ================== 29 | * :ref:`genindex` 30 | * :ref:`modindex` 31 | * :ref:`search` 32 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=python -msphinx 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=aicsimageio 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The Sphinx module was not found. Make sure you have Sphinx installed, 20 | echo.then set the SPHINXBUILD environment variable to point to the full 21 | echo.path of the 'sphinx-build' executable. Alternatively you may add the 22 | echo.Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | aicsimageio 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | aicsimageio 8 | -------------------------------------------------------------------------------- /presentations/2021-dask-life-sciences/environment.yml: -------------------------------------------------------------------------------- 1 | name: aicsimageio-dask-summit 2 | channels: 3 | - defaults 4 | dependencies: 5 | - graphviz 6 | - pip: 7 | - aicsimageio==4.0.0.dev6 8 | - dask==2021.4.1 9 | - dask-image==0.6.0 10 | - distributed==2021.4.1 11 | - graphviz==0.16 12 | - jupyter-contrib-nbextensions==0.5.1 13 | - jupyterlab==3.0.14 14 | - matplotlib==3.4.2 15 | - nbconvert==6.0.7 16 | -------------------------------------------------------------------------------- /scripts/TEST_RESOURCES_HASH.txt: -------------------------------------------------------------------------------- 1 | 4e3100e86a7fb0a6d1d6f38d1bd04b98165dbcea2323a8130bf16525a532d65d 2 | -------------------------------------------------------------------------------- /scripts/download_test_resources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import logging 6 | import sys 7 | import traceback 8 | from pathlib import Path 9 | 10 | from quilt3 import Package 11 | 12 | ############################################################################### 13 | 14 | logging.basicConfig( 15 | level=logging.INFO, 16 | format="[%(levelname)4s: %(module)s:%(lineno)4s %(asctime)s] %(message)s", 17 | ) 18 | log = logging.getLogger(__name__) 19 | 20 | ############################################################################### 21 | # Args 22 | 23 | 24 | class Args(argparse.Namespace): 25 | def __init__(self): 26 | self.__parse() 27 | 28 | def __parse(self): 29 | # Setup parser 30 | p = argparse.ArgumentParser( 31 | prog="download_test_resources", 32 | description=( 33 | "Download files used for testing this project. This will download " 34 | "all the required test resources and place them in the " 35 | "`tests/resources` directory." 36 | ), 37 | ) 38 | 39 | # Arguments 40 | p.add_argument( 41 | "--top-hash", 42 | # Generated package hash from upload_test_resources 43 | default=None, 44 | help=( 45 | "A specific version of the package to retrieve. " 46 | "If none, will read from the TEST_RESOURCES_HASH.txt file." 47 | ), 48 | ) 49 | p.add_argument( 50 | "--debug", 51 | action="store_true", 52 | help="Show traceback if the script were to fail.", 53 | ) 54 | 55 | # Parse 56 | p.parse_args(namespace=self) 57 | 58 | 59 | ############################################################################### 60 | # Build package 61 | 62 | 63 | def download_test_resources(args: Args): 64 | # Try running the download pipeline 65 | try: 66 | # Get test resources dir 67 | resources_dir = ( 68 | Path(__file__).parent.parent / "aicsimageio" / "tests" / "resources" 69 | ).resolve() 70 | resources_dir.mkdir(exist_ok=True) 71 | 72 | # Use or read top hash 73 | if args.top_hash is None: 74 | with open(Path(__file__).parent / "TEST_RESOURCES_HASH.txt", "r") as f: 75 | top_hash = f.readline().rstrip() 76 | else: 77 | top_hash = args.top_hash 78 | 79 | log.info(f"Downloading test resources using top hash: {top_hash}") 80 | 81 | # Get quilt package 82 | package = Package.browse( 83 | "aicsimageio/test_resources", 84 | "s3://aics-modeling-packages-test-resources", 85 | top_hash=top_hash, 86 | ) 87 | 88 | # Download 89 | package["resources"].fetch(resources_dir) 90 | 91 | log.info(f"Completed package download.") 92 | 93 | # Catch any exception 94 | except Exception as e: 95 | log.error("=============================================") 96 | if args.debug: 97 | log.error("\n\n" + traceback.format_exc()) 98 | log.error("=============================================") 99 | log.error("\n\n" + str(e) + "\n") 100 | log.error("=============================================") 101 | sys.exit(1) 102 | 103 | 104 | ############################################################################### 105 | # Runner 106 | 107 | 108 | def main(): 109 | args = Args() 110 | download_test_resources(args) 111 | 112 | 113 | ############################################################################### 114 | # Allow caller to directly run this module (usually in development scenarios) 115 | 116 | if __name__ == "__main__": 117 | main() 118 | -------------------------------------------------------------------------------- /scripts/makezarr.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "1908a9c2-31de-46ba-b461-fa81f515487b", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import os\n", 11 | "import s3fs\n", 12 | "from aicsimageio.writers import OmeZarrWriter\n", 13 | "from aicsimageio import AICSImage\n", 14 | "from aicsimageio.dimensions import DimensionNames, DEFAULT_CHUNK_DIMS\n", 15 | "import numpy" 16 | ] 17 | }, 18 | { 19 | "cell_type": "code", 20 | "execution_count": null, 21 | "id": "7b5001be", 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "# set up some initial vars to find our data and where to put it\n", 26 | "\n", 27 | "filepath = \"my/path/to/data/file.tif\"\n", 28 | "output_filename = \"my_filename\"\n", 29 | "output_bucket = \"my_bucket\"\n", 30 | "# aws config\n", 31 | "os.environ[\"AWS_PROFILE\"] = \"my_creds\"\n", 32 | "os.environ[\"AWS_DEFAULT_REGION\"] = \"us-west-2\"\n", 33 | "\n" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "id": "d8847835-b420-48b1-9df2-09078e9a9b05", 40 | "metadata": { 41 | "tags": [] 42 | }, 43 | "outputs": [], 44 | "source": [ 45 | "# allow for dask parallelism\n", 46 | "from distributed import LocalCluster, Client\n", 47 | "cluster = LocalCluster(n_workers=4, processes=True, threads_per_worker=1)\n", 48 | "client = Client(cluster)\n", 49 | "client" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": null, 55 | "id": "561c6a3d-8501-41c2-982b-a546d0e6c814", 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "# load our image\n", 60 | "\n", 61 | "chunk_dims= [\n", 62 | " DimensionNames.SpatialY,\n", 63 | " DimensionNames.SpatialX,\n", 64 | " DimensionNames.Samples,\n", 65 | "]\n", 66 | "img = AICSImage(filepath, chunk_dims=chunk_dims)\n", 67 | "\n", 68 | "# print some data about the image we loaded\n", 69 | "scenes = img.scenes\n", 70 | "print(scenes)\n", 71 | "print(str(len(scenes)))\n", 72 | "print(img.channel_names)\n", 73 | "print(img.physical_pixel_sizes)\n", 74 | "\n" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": null, 80 | "id": "4ce00837-9e95-459c-9512-0fef22d896e1", 81 | "metadata": {}, 82 | "outputs": [], 83 | "source": [ 84 | "# construct some per-channel lists to feed in to the writer.\n", 85 | "# hardcoding to 9 for now\n", 86 | "channel_colors = [0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff, 0x880000, 0x008800, 0x000088]\n", 87 | "\n", 88 | "# initialize for writing direct to S3\n", 89 | "s3 = s3fs.S3FileSystem(anon=False, config_kwargs={\"connect_timeout\": 60})\n", 90 | "\n", 91 | "def write_scene(storeroot, scenename, sceneindex, img):\n", 92 | " print(scenename)\n", 93 | " print(storeroot)\n", 94 | " img.set_scene(sceneindex)\n", 95 | " pps = img.physical_pixel_sizes\n", 96 | " cn = img.channel_names\n", 97 | "\n", 98 | " data = img.get_image_dask_data(\"TCZYX\")\n", 99 | " print(data.shape)\n", 100 | " writer = OmeZarrWriter(storeroot)\n", 101 | "\n", 102 | " writer.write_image(\n", 103 | " image_data=data,\n", 104 | " image_name=scenename,\n", 105 | " physical_pixel_sizes=pps,\n", 106 | " channel_names=cn,\n", 107 | " channel_colors=channel_colors,\n", 108 | " scale_num_levels=4,\n", 109 | " scale_factor=2.0,\n", 110 | " ) \n", 111 | "\n", 112 | "# here we are splitting multi-scene images into separate zarr images based on scene name\n", 113 | "scene_indices = range(len(img.scenes))\n", 114 | "\n", 115 | "for i in scene_indices:\n", 116 | " scenename = img.scenes[i]\n", 117 | " scenename = scenename.replace(\":\",\"_\")\n", 118 | " write_scene(f\"s3://{output_bucket}/{output_filename}/{scenename}.zarr/\", scenename, i, img)\n" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "id": "7ddfd030", 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "#############################################\n", 129 | "# READ BACK\n", 130 | "#############################################\n", 131 | "from ome_zarr.reader import Multiscales, Reader\n", 132 | "from ome_zarr.io import parse_url\n", 133 | "\n", 134 | "s3 = s3fs.S3FileSystem()\n", 135 | "mypath = f\"s3://{output_bucket}/{output_filename}/{scenes[scene_indices[0]]}.zarr\" \n", 136 | "\n", 137 | "reader = Reader(parse_url(mypath))\n", 138 | "node = list(reader())[0]\n", 139 | "# levels\n", 140 | "print(len(node.data))\n", 141 | "for i in range(len(node.data)):\n", 142 | " print(f\"shape of level {i} : {node.data[i].shape}\")\n" 143 | ] 144 | }, 145 | { 146 | "cell_type": "code", 147 | "execution_count": null, 148 | "id": "00bff414", 149 | "metadata": {}, 150 | "outputs": [], 151 | "source": [ 152 | "import nbvv\n", 153 | "level = 1\n", 154 | "levelxyscale = 2**(level+1)\n", 155 | "t = 0\n", 156 | "readdata = node.data[level][t][0:2].compute()\n", 157 | "print(readdata.shape)\n", 158 | "nbvv.volshow(readdata, spacing=(img.physical_pixel_sizes.X*levelxyscale,\n", 159 | " img.physical_pixel_sizes.Y*levelxyscale,\n", 160 | " img.physical_pixel_sizes.Z))" 161 | ] 162 | }, 163 | { 164 | "cell_type": "code", 165 | "execution_count": null, 166 | "id": "8f5765d5", 167 | "metadata": {}, 168 | "outputs": [], 169 | "source": [] 170 | } 171 | ], 172 | "metadata": { 173 | "kernelspec": { 174 | "display_name": "Python 3", 175 | "language": "python", 176 | "name": "python3" 177 | }, 178 | "language_info": { 179 | "codemirror_mode": { 180 | "name": "ipython", 181 | "version": 3 182 | }, 183 | "file_extension": ".py", 184 | "mimetype": "text/x-python", 185 | "name": "python", 186 | "nbconvert_exporter": "python", 187 | "pygments_lexer": "ipython3", 188 | "version": "3.8.5" 189 | }, 190 | "toc-autonumbering": false, 191 | "toc-showcode": false, 192 | "toc-showmarkdowntxt": false, 193 | "toc-showtags": true, 194 | "vscode": { 195 | "interpreter": { 196 | "hash": "acd95a8530237d9a6e58775572ad6f18eaa27400a7c3d8072e8f8672de94478f" 197 | } 198 | } 199 | }, 200 | "nbformat": 4, 201 | "nbformat_minor": 5 202 | } 203 | -------------------------------------------------------------------------------- /scripts/upload_test_resources.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import logging 6 | import sys 7 | import traceback 8 | from pathlib import Path 9 | 10 | from quilt3 import Package 11 | 12 | from aicsimageio import __version__ 13 | 14 | ############################################################################### 15 | 16 | logging.basicConfig( 17 | level=logging.INFO, 18 | format="[%(levelname)4s: %(module)s:%(lineno)4s %(asctime)s] %(message)s", 19 | ) 20 | log = logging.getLogger(__name__) 21 | 22 | ############################################################################### 23 | # Args 24 | 25 | 26 | class Args(argparse.Namespace): 27 | def __init__(self): 28 | self.__parse() 29 | 30 | def __parse(self): 31 | # Setup parser 32 | p = argparse.ArgumentParser( 33 | prog="upload_test_resources", 34 | description=( 35 | "Upload files used for testing this project. This will upload " 36 | "whatever files are currently found in the `tests/resources` directory." 37 | "To add more test files, simply add them to the `tests/resources` " 38 | "directory and rerun this script." 39 | ), 40 | ) 41 | 42 | # Arguments 43 | p.add_argument( 44 | "--dry-run", 45 | action="store_true", 46 | help=( 47 | "Conduct dry run of the package generation. Will create a JSON " 48 | "manifest file of that package instead of uploading." 49 | ), 50 | ) 51 | p.add_argument( 52 | "-y", 53 | "--yes", 54 | action="store_true", 55 | dest="preapproved", 56 | help="Auto-accept upload of files.", 57 | ) 58 | p.add_argument( 59 | "--debug", 60 | action="store_true", 61 | help="Show traceback if the script were to fail.", 62 | ) 63 | 64 | # Parse 65 | p.parse_args(namespace=self) 66 | 67 | 68 | ############################################################################### 69 | # Build package 70 | 71 | 72 | def upload_test_resources(args: Args): 73 | # Try running the download pipeline 74 | try: 75 | # Get test resources dir 76 | resources_dir = ( 77 | Path(__file__).parent.parent / "aicsimageio" / "tests" / "resources" 78 | ).resolve(strict=True) 79 | 80 | # Report with directory will be used for upload 81 | log.info(f"Using contents of directory: {resources_dir}") 82 | 83 | # Create quilt package 84 | package = Package() 85 | package.set_dir("resources", resources_dir) 86 | 87 | # Report package contents 88 | log.info(f"Package contents: {package}") 89 | 90 | # Construct package name 91 | package_name = "aicsimageio/test_resources" 92 | 93 | # Check for dry run 94 | if args.dry_run: 95 | # Attempt to build the package 96 | top_hash = package.build(package_name) 97 | 98 | # Get resolved save path 99 | manifest_save_path = Path("upload_manifest.jsonl").resolve() 100 | with open(manifest_save_path, "w") as manifest_write: 101 | package.dump(manifest_write) 102 | 103 | # Report where manifest was saved 104 | log.info(f"Dry run generated manifest stored to: {manifest_save_path}") 105 | log.info(f"Completed package dry run. Result hash: {top_hash}") 106 | 107 | # Upload 108 | else: 109 | # Check pre-approved push 110 | if args.preapproved: 111 | confirmation = True 112 | else: 113 | # Get upload confirmation 114 | confirmation = None 115 | while confirmation is None: 116 | # Get user input 117 | user_input = input("Upload [y]/n? ") 118 | 119 | # If the user simply pressed enter assume yes 120 | if len(user_input) == 0: 121 | user_input = "y" 122 | # Get first character and lowercase 123 | else: 124 | user_input = user_input[0].lower() 125 | 126 | # Set confirmation from None to a value 127 | if user_input == "y": 128 | confirmation = True 129 | elif user_input == "n": 130 | confirmation = False 131 | 132 | # Check confirmation 133 | if confirmation: 134 | pushed = package.push( 135 | package_name, 136 | "s3://aics-modeling-packages-test-resources", 137 | message=f"Test resources for `aicsimageio` version: {__version__}.", 138 | ) 139 | 140 | log.info(f"Completed package push. Result hash: {pushed.top_hash}") 141 | else: 142 | log.info(f"Upload canceled.") 143 | 144 | # Catch any exception 145 | except Exception as e: 146 | log.error("=============================================") 147 | if args.debug: 148 | log.error("\n\n" + traceback.format_exc()) 149 | log.error("=============================================") 150 | log.error("\n\n" + str(e) + "\n") 151 | log.error("=============================================") 152 | sys.exit(1) 153 | 154 | 155 | ############################################################################### 156 | # Runner 157 | 158 | 159 | def main(): 160 | args = Args() 161 | upload_test_resources(args) 162 | 163 | 164 | ############################################################################### 165 | # Allow caller to directly run this module (usually in development scenarios) 166 | 167 | if __name__ == "__main__": 168 | main() 169 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 4.14.0 3 | parse = (?P\d+)\.(?P\d+)\.(?P\d+) 4 | serialize = {major}.{minor}.{patch} 5 | commit = True 6 | tag = True 7 | 8 | [bumpversion:file:setup.py] 9 | search = version="{current_version}" 10 | replace = version="{new_version}" 11 | 12 | [bumpversion:file:aicsimageio/__init__.py] 13 | search = {current_version} 14 | replace = {new_version} 15 | 16 | [bumpversion:file:cookiecutter.yaml] 17 | search = {current_version} 18 | replace = {new_version} 19 | 20 | [bdist_wheel] 21 | universal = 1 22 | 23 | [aliases] 24 | test = pytest 25 | 26 | [tool:pytest] 27 | collect_ignore = ['setup.py'] 28 | xfail_strict = true 29 | filterwarnings = 30 | ignore::UserWarning 31 | ignore::FutureWarning 32 | ignore:distutils Version classes are deprecated: 33 | addopts = -p no:faulthandler 34 | 35 | [flake8] 36 | exclude = 37 | docs/ 38 | aicsimageio/vendor/ 39 | ignore = 40 | E203 41 | E402 42 | W291 43 | W503 44 | max-line-length = 88 45 | 46 | [isort] 47 | profile = black 48 | multi_line_output = 3 49 | include_trailing_comma = True 50 | force_grid_wrap = 0 51 | use_parentheses = True 52 | ensure_newline_before_comments = True 53 | line_length = 88 54 | 55 | [mypy] 56 | ignore_missing_imports = True 57 | disallow_untyped_defs = True 58 | check_untyped_defs = True 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | from setuptools import find_packages, setup 7 | from setuptools.command.build_py import build_py 8 | from pathlib import Path 9 | from typing import Dict, List 10 | 11 | 12 | class BuildPyCommand(build_py): 13 | """Check for existence of XSLT before building.""" 14 | 15 | def run(self): 16 | xslt = ( 17 | Path(__file__).parent 18 | / "aicsimageio/metadata/czi-to-ome-xslt/xslt/czi-to-ome.xsl" 19 | ) 20 | if not xslt.is_file(): 21 | raise FileNotFoundError("XSLT not found. Is the submodule checked out?") 22 | build_py.run(self) 23 | 24 | 25 | with open("README.md") as readme_file: 26 | readme = readme_file.read() 27 | 28 | 29 | # If you need to update a version pin for one of these supporting format libs, 30 | # be sure to also check if you need to update these versions in the 31 | # "READER_TO_INSTALL" lookup table from aicsimageio/formats.py. 32 | format_libs: Dict[str, List[str]] = { 33 | "base-imageio": [ 34 | "imageio[ffmpeg]>=2.31.0", 35 | "Pillow>=9.3.0", 36 | ], 37 | "nd2": ["nd2[legacy]>=0.6.0"], 38 | "dv": ["mrc>=0.2.0"], 39 | "bfio": ["bfio>=2.3.1"], 40 | # "czi": [ # excluded for licensing reasons 41 | # "fsspec>=2022.8.0", 42 | # "aicspylibczi>=3.1.1", 43 | # ], 44 | # "bioformats": ["bioformats_jar"], # excluded for licensing reasons 45 | # "lif": ["readlif>=0.6.4"], # excluded for licensing reasons 46 | } 47 | 48 | all_formats: List[str] = [] 49 | for deps in format_libs.values(): 50 | for dep in deps: 51 | all_formats.append(dep) 52 | 53 | setup_requirements = [ 54 | "pytest-runner>=5.2", 55 | ] 56 | 57 | test_requirements = [ 58 | "dask[array,distributed]>=2021.4.1,!=2022.5.1", 59 | "docutils>=0.10,<0.16", 60 | "psutil>=5.7.0", 61 | "pytest>=5.4.3", 62 | "pytest-cov>=2.9.0", 63 | "pytest-raises>=0.11", 64 | "quilt3", # no pin to avoid pip cycling (boto is really hard to manage) 65 | "s3fs[boto3]>=2022.11.0", 66 | "tox==3.27.1", 67 | ] 68 | 69 | dev_requirements = [ 70 | *setup_requirements, 71 | *test_requirements, 72 | "asv>=0.4.2", 73 | "black>=22.3.0", 74 | "bump2version>=1.0.1", 75 | "coverage>=5.1", 76 | "flake8>=3.8.3", 77 | "flake8-debugger>=3.2.1", 78 | "gitchangelog>=3.0.4", 79 | "ipython>=7.15.0", 80 | "isort>=5.11.5", 81 | "m2r2>=0.2.7", 82 | "mypy>=0.800", 83 | "pytest-runner>=5.2", 84 | "Sphinx>=3.4.3", 85 | "sphinx_rtd_theme>=0.5.1", 86 | "twine>=3.1.1", 87 | "types-PyYAML>=6.0.12.9", 88 | "wheel>=0.34.2", 89 | # reader deps 90 | *all_formats, 91 | "bioformats_jar", # to test bioformats 92 | "bfio>=2.3.0", 93 | "readlif>=0.6.4", # to test lif 94 | "aicspylibczi>=3.1.1", # to test czi 95 | ] 96 | 97 | benchmark_requirements = [ 98 | *dev_requirements, 99 | "dask-image>=0.6.0", 100 | ] 101 | 102 | requirements = [ 103 | "dask[array]>=2021.4.1", 104 | # fssspec restricted due to glob issue tracked here, when fixed remove ceiling 105 | # https://github.com/fsspec/filesystem_spec/issues/1380 106 | "fsspec>=2022.8.0,<2023.9.0", 107 | "imagecodecs>=2020.5.30", 108 | "lxml>=4.6,<5", 109 | "numpy>=1.21.0", 110 | "ome-types>=0.3.4", 111 | "ome-zarr>=0.6.1", 112 | "PyYAML>=6.0", 113 | "wrapt>=1.12", 114 | "resource-backed-dask-array>=0.1.0", 115 | "tifffile>=2021.8.30,<2023.3.15", 116 | "xarray>=0.16.1", 117 | "xmlschema", # no pin because it's pulled in from OME types 118 | "zarr>=2.6,<2.16.0", 119 | ] 120 | 121 | extra_requirements = { 122 | "setup": setup_requirements, 123 | "test": test_requirements, 124 | "dev": dev_requirements, 125 | "benchmark": benchmark_requirements, 126 | **format_libs, 127 | "all": all_formats, 128 | } 129 | 130 | setup( 131 | author="Eva Maxfield Brown, Allen Institute for Cell Science", 132 | author_email="evamaxfieldbrown@gmail.com, jamie.sherman@gmail.com, bowdenm@spu.edu", 133 | cmdclass={"build_py": BuildPyCommand}, 134 | classifiers=[ 135 | "Development Status :: 5 - Production/Stable", 136 | "Intended Audience :: Science/Research", 137 | "Intended Audience :: Developers", 138 | "Intended Audience :: Education", 139 | "License :: OSI Approved :: BSD License", 140 | "Natural Language :: English", 141 | "Programming Language :: Python :: 3.9", 142 | "Programming Language :: Python :: 3.10", 143 | "Programming Language :: Python :: 3.11", 144 | ], 145 | description=( 146 | "Image Reading, Metadata Conversion, and Image Writing for Microscopy Images " 147 | "in Pure Python" 148 | ), 149 | entry_points={}, 150 | install_requires=requirements, 151 | license="BSD-3-Clause", 152 | long_description=readme, 153 | long_description_content_type="text/markdown", 154 | include_package_data=True, 155 | keywords="imageio, image reading, image writing, metadata, microscopy, allen cell", 156 | name="aicsimageio", 157 | packages=find_packages( 158 | exclude=[ 159 | "tests", 160 | "*.tests", 161 | "*.tests.*", 162 | "benchmarks", 163 | "*.benchmarks", 164 | "*.benchmarks.*", 165 | ] 166 | ), 167 | python_requires=">=3.9", 168 | setup_requires=setup_requirements, 169 | test_suite="aicsimageio/tests", 170 | tests_require=test_requirements, 171 | extras_require=extra_requirements, 172 | url="https://github.com/AllenCellModeling/aicsimageio", 173 | # Do not edit this string manually, always use bumpversion 174 | # Details in CONTRIBUTING.md 175 | version="4.14.0", 176 | zip_safe=False, 177 | ) 178 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | 3 | envlist = py38, py39, py310, py311, bioformats, czi, base-imageio, dv, lif, nd2, omezarr, sldy, bfio, upstreams, lint 4 | skip_missing_interpreters = true 5 | toxworkdir={env:TOX_WORK_DIR:.tox} 6 | 7 | [testenv:lint] 8 | skip_install = true 9 | deps = pre-commit 10 | commands = pre-commit run --all-files --show-diff-on-failure 11 | 12 | [testenv:bioformats] 13 | passenv = 14 | AWS_* 15 | JAVA_HOME 16 | CI 17 | setenv = 18 | PYTHONPATH = {toxinidir} 19 | extras = 20 | test 21 | deps = 22 | bioformats_jar 23 | commands = 24 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/test_bioformats_reader.py {posargs} 25 | 26 | [testenv:czi] 27 | passenv = 28 | AWS_* 29 | JAVA_HOME 30 | CI 31 | setenv = 32 | PYTHONPATH = {toxinidir} 33 | extras = 34 | test 35 | deps = 36 | aicspylibczi>=3.1.1 37 | fsspec>=2022.8.0 38 | commands = 39 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/test_czi_reader.py {posargs} 40 | 41 | [testenv:base-imageio] 42 | passenv = 43 | AWS_* 44 | JAVA_HOME 45 | CI 46 | setenv = 47 | PYTHONPATH = {toxinidir} 48 | extras = 49 | test 50 | base-imageio 51 | commands = 52 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/test_default_reader.py aicsimageio/tests/writers/extra_writers/test_timeseries_writer.py aicsimageio/tests/writers/extra_writers/test_two_d_writer.py {posargs} 53 | 54 | [testenv:dv] 55 | passenv = 56 | AWS_* 57 | JAVA_HOME 58 | CI 59 | setenv = 60 | PYTHONPATH = {toxinidir} 61 | extras = 62 | test 63 | dv 64 | commands = 65 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/test_dv_reader.py {posargs} 66 | 67 | [testenv:lif] 68 | passenv = 69 | AWS_* 70 | JAVA_HOME 71 | CI 72 | setenv = 73 | PYTHONPATH = {toxinidir} 74 | extras = 75 | test 76 | deps = readlif>=0.6.4 77 | commands = 78 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/test_lif_reader.py {posargs} 79 | 80 | [testenv:nd2] 81 | passenv = 82 | AWS_* 83 | JAVA_HOME 84 | CI 85 | setenv = 86 | PYTHONPATH = {toxinidir} 87 | extras = 88 | test 89 | nd2 90 | commands = 91 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/test_nd2_reader.py {posargs} 92 | 93 | [testenv:sldy] 94 | passenv = 95 | AWS_* 96 | JAVA_HOME 97 | CI 98 | setenv = 99 | PYTHONPATH = {toxinidir} 100 | extras = 101 | test 102 | commands = 103 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/sldy_reader/ {posargs} 104 | 105 | [testenv:bfio] 106 | passenv = 107 | AWS_* 108 | JAVA_HOME 109 | CI 110 | setenv = 111 | PYTHONPATH = {toxinidir} 112 | extras = 113 | test 114 | bfio 115 | commands = 116 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/test_ome_tiled_tiff_reader.py {posargs} 117 | 118 | [testenv:omezarr] 119 | passenv = 120 | AWS_* 121 | JAVA_HOME 122 | CI 123 | setenv = 124 | PYTHONPATH = {toxinidir} 125 | extras = 126 | test 127 | commands = 128 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/readers/extra_readers/test_ome_zarr_reader.py {posargs} 129 | 130 | [testenv:upstreams] 131 | passenv = 132 | AWS_* 133 | JAVA_HOME 134 | CI 135 | setenv = 136 | PYTHONPATH = {toxinidir} 137 | extras = 138 | test 139 | deps = 140 | dask @ git+https://github.com/dask/dask@main 141 | distributed @ git+https://github.com/dask/distributed@main 142 | fsspec @ git+https://github.com/fsspec/filesystem_spec@master 143 | s3fs @ git+https://github.com/fsspec/s3fs@main 144 | xarray @ git+https://github.com/pydata/xarray@main 145 | commands = 146 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/ --ignore-glob="**/extra_readers/*" --ignore-glob="**/extra_writers/*" {posargs} 147 | 148 | [testenv] 149 | passenv = 150 | AWS_* 151 | JAVA_HOME 152 | CI 153 | setenv = 154 | PYTHONPATH = {toxinidir} 155 | extras = 156 | test 157 | commands = 158 | pytest --basetemp={envtmpdir} --cov-report xml --cov-report html --cov=aicsimageio aicsimageio/tests/ --ignore-glob="**/extra_readers/*" --ignore-glob="**/extra_writers/*" {posargs} 159 | --------------------------------------------------------------------------------