diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 000000000..62dffdd3f --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: f5bd3289deda50bf581aa3d97e85c32f +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..5cee661d4 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,461 @@ +--- +version: 2.1 +executors: + toxandnode: + working_directory: ~/project + docker: + - image: girder/tox-and-node +commands: + tox: + description: "Run tox" + parameters: + env: + type: string + steps: + - run: + name: Upgrade pip + command: pip install -U pip + - run: + name: Upgrade virtualenv and tox + command: pip install -U virtualenv tox + # - run: + # name: Preinstall phantomjs to work around an npm permission issue + # command: npm install -g phantomjs-prebuilt --unsafe-perm + - run: + name: Run tests via tox + # Piping through cat does less buffering of the output but can + # consume the exit code + # command: PYTEST_ADDOPTS=--forked tox -e << parameters.env >> | cat; test ${PIPESTATUS[0]} -eq 0 + # command: PYTEST_ADDOPTS="--reruns=3 --numprocesses=0" tox -e << parameters.env >> | cat; test ${PIPESTATUS[0]} -eq 0 + command: COVERAGE_CORE=sysmon PYTEST_NUMPROCESSES=3 PYTEST_ADDOPTS="--reruns=3" tox -e << parameters.env >> | cat; test ${PIPESTATUS[0]} -eq 0 + switchpython: + description: "Upgrade python" + parameters: + version: + type: string + steps: + - run: + name: Upgrade pyenv + command: | + sudo rm -rf /opt/circleci/.pyenv + curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash + pyenv install --list list + - run: + name: Use pyenv to install python + command: | + pyenv install -s << parameters.version >> + - run: + name: Use pyenv to set python version + command: | + pyenv versions + pyenv global << parameters.version >> + allservices: + description: "Switch to a python version and start other services" + parameters: + version: + type: string + node: + type: string + steps: + - switchpython: + version: << parameters.version >> + - run: + name: start mongo + # This had been + # docker run --rm -d -p 27017:27017 circleci/mongo:5.0-ram + # but circleci has deprecated their mongo images. Running as ram + # just turned off journalling and run with the db on a memory mapped + # location. --bind_ip_all is required. + command: | + # docker run --rm -d -p 127.0.0.1:27017:27017 mongo:5.0 bash -c "mkdir /dev/shm/mongo && mongod --nojournal --dbpath=/dev/shm/mongo --noauth --bind_ip_all" + docker run --rm -d -p 127.0.0.1:27017:27017 mongo:latest bash -c "mongod --noauth --bind_ip_all" + - run: + name: start dcm4chee and upload example data (for DICOMweb tests) + command: | + docker-compose -f ./.circleci/dcm4chee/auth-docker-compose.yml up -d + export DICOMWEB_TEST_URL=http://localhost:8008/dcm4chee-arc/aets/DCM4CHEE/rs + echo "export DICOMWEB_TEST_URL=$DICOMWEB_TEST_URL" >> $BASH_ENV + pip install dicomweb_client 'python-keycloak<4.1' + + # Wait up to 60 seconds for keycloak to be ready + echo 'Waiting for keycloak to start...' + KEYCLOAK_URL=https://localhost:8843 + curl -k --retry 60 -f --retry-all-errors --retry-delay 1 -s -o /dev/null $KEYCLOAK_URL + echo 'Updating keycloak token lifespan...' + python -W ignore ./.circleci/dcm4chee/update_access_token_lifespan.py + echo 'Creating keycloak access token...' + # Now create the token + export DICOMWEB_TEST_TOKEN=$(python -W ignore ./.circleci/dcm4chee/create_keycloak_token.py) + echo "export DICOMWEB_TEST_TOKEN=$DICOMWEB_TEST_TOKEN" >> $BASH_ENV + + # Wait up to 30 seconds for the server if it isn't ready + echo 'Waiting for dcm4chee to start...' + curl --header "Authorization: Bearer $DICOMWEB_TEST_TOKEN" --retry 30 -f --retry-all-errors --retry-delay 1 -s -o /dev/null $DICOMWEB_TEST_URL/studies + + # Upload the example data + echo 'Uploading example data...' + python ./.circleci/dcm4chee/upload_example_data.py + - run: + name: start rabbitmq + command: | + docker run --rm -d -p 5672:5672 rabbitmq + - run: + name: start memcached + command: | + docker run --rm -d -p 11211:11211 memcached -m 64 + - run: + name: start redis + command: | + docker run --rm -d -p 6379:6379 redis + echo "export REDIS_TEST_URL=127.0.01:6379" >> $BASH_ENV + - run: + name: Use nvm + # see https://discuss.circleci.com/t/nvm-does-not-change-node-version-on-machine/28973/14 + command: | + echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV + echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV + - run: + name: Switch node versions + command: | + nvm install << parameters.node >> + nvm alias default << parameters.node >> + NODE_DIR=$(dirname $(which node)) + echo "export PATH=$NODE_DIR:\$PATH" >> $BASH_ENV + - run: + name: Check node versions + command: | + node --version + npm --version + coverage: + description: "Upload coverage" + steps: + - run: + name: Install Codecov client + command: | + curl -Os https://uploader.codecov.io/latest/linux/codecov + chmod +x codecov + - run: + name: Upload coverage + command: | + ./codecov --disable search pycov gcov --file build/test/coverage/py_coverage.xml,build/test/coverage/cobertura-coverage.xml +jobs: + testdocker: + machine: + image: ubuntu-2204:current + steps: + - checkout + - run: + name: Build the test docker + command: docker build --force-rm -t girder/tox-and-node -f test.Dockerfile . + - run: + name: Publish the images to Docker Hub + command: | + echo "$DOCKERHUB_PASS" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin + docker push girder/tox-and-node:latest + py38: + machine: + image: ubuntu-2204:current + resource_class: large + steps: + - checkout + - allservices: + version: "3.8" + node: v14 + - tox: + env: test-py38 + - coverage + - store_artifacts: + path: build/test/artifacts + py39: + machine: + image: ubuntu-2204:current + resource_class: large + steps: + - checkout + - allservices: + version: "3.9" + node: v14 + - tox: + env: test-py39 + - coverage + - store_artifacts: + path: build/test/artifacts + py310: + machine: + image: ubuntu-2204:current + resource_class: large + steps: + - checkout + - allservices: + version: "3.10" + node: v14 + - tox: + env: test-py310 + - coverage + - store_artifacts: + path: build/test/artifacts + py311: + machine: + image: ubuntu-2204:current + resource_class: large + steps: + - checkout + - allservices: + version: "3.11" + node: v14 + - tox: + env: test-py311 + - coverage + - store_artifacts: + path: build/test/artifacts + py312: + machine: + image: ubuntu-2204:current + resource_class: large + steps: + - checkout + - allservices: + version: "3.12" + node: v14 + - tox: + env: test-py312 + - coverage + - store_artifacts: + path: build/test/artifacts + lint_and_docs: + executor: toxandnode + steps: + - checkout + - run: + name: Install dependencies + command: apt-get update -yq && apt-get install -yq pandoc && pandoc --version + - run: + name: Permissions for link checker + command: find /root -type d -exec chmod 755 {} \+ + - tox: + env: docs,lint,lintclient,notebook + - store_artifacts: + path: build/docs + - persist_to_workspace: + root: build + paths: docs + compare: + executor: toxandnode + resource_class: large + steps: + - checkout + - tox: + env: compare-py311 + - store_artifacts: + path: build/tox/compare.txt + - store_artifacts: + path: build/tox/compare.yaml + type: + executor: toxandnode + resource_class: large + steps: + - checkout + - tox: + env: type + wheels: + executor: toxandnode + steps: + - checkout + - run: + name: Build wheels + command: ./.circleci/make_wheels.sh + - run: + name: Make index file + command: python ./.circleci/make_index.py ~/wheels + - store_artifacts: + path: ~/wheels + check_release: + docker: + - image: cimg/python:3.10 + steps: + - checkout + - run: + name: Setup virtual environment + command: | + if [ ! -d env ]; then python -m virtualenv env || python -m venv env; fi + echo ". $CIRCLE_WORKING_DIRECTORY/env/bin/activate" >> $BASH_ENV + - run: + name: Install python packages + command: pip install setuptools_scm twine + - run: + name: Check release to PyPi + command: ./.circleci/release_pypi.sh check + release: + docker: + - image: cimg/python:3.10 + steps: + - checkout + - run: + name: Setup virtual environment + command: | + if [ ! -d env ]; then python -m virtualenv env || python -m venv env; fi + echo ". $CIRCLE_WORKING_DIRECTORY/env/bin/activate" >> $BASH_ENV + - run: + name: Install python packages + command: pip install setuptools_scm twine + - run: + name: Release to PyPi + command: ./.circleci/release_pypi.sh upload + docs-deploy: + working_directory: ~/project + docker: + - image: node + steps: + - checkout + - attach_workspace: + at: build + - run: + name: Disable jekyll builds + command: touch build/docs/.nojekyll + - run: + name: Install and configure dependencies + command: | + npm install -g --silent 'gh-pages@<3.2.1||>3.2.1' + git config user.email "ci-build@kitware.com" + git config user.name "ci-build" + - add_ssh_keys: + fingerprints: + - "a4:7a:f8:e9:19:61:88:9b:d8:af:50:b8:32:9f:03:29" + - run: + name: Deploy docs to gh-pages branch + command: | + touch package.json + gh-pages --dotfiles --message "Update documentation" --dist build/docs --no-history +workflows: + version: 2 + ci: + jobs: + - testdocker: + filters: + branches: + only: + - master + # Create a branch of this name to push to docker hub + - testdocker + - py38: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - py39: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - py310: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - py311: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - py312: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - lint_and_docs: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - type: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - compare: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - wheels: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - check_release: + filters: + tags: + only: /^v.*/ + branches: + ignore: + - gh-pages + - release: + requires: + - check_release + - py38 + - py39 + - py310 + - py311 + - py312 + - lint_and_docs + - type + - wheels + filters: + tags: + only: /^v.*/ + branches: + only: master + - docs-deploy: + requires: + - py38 + - py39 + - py310 + - py311 + - py312 + - lint_and_docs + - type + - wheels + filters: + tags: + only: /^v.*/ + branches: + only: + - master + - sphinx + periodic: + triggers: + - schedule: + # Run every Monday morning at 3 a.m. + cron: "0 3 * * 1" + filters: + branches: + only: + - master + jobs: + - py38 + - py39 + - py310 + - py311 + - py312 + - lint_and_docs + - type + - compare + - wheels diff --git a/.circleci/dcm4chee/auth-docker-compose.yml b/.circleci/dcm4chee/auth-docker-compose.yml new file mode 100644 index 000000000..f569e6635 --- /dev/null +++ b/.circleci/dcm4chee/auth-docker-compose.yml @@ -0,0 +1,98 @@ +--- +volumes: + db_data: {} + arc_data: {} + ldap_data: {} + ldap_config: {} + mysql: {} + keycloak: {} +services: + ldap: + image: dcm4che/slapd-dcm4chee:2.6.5-31.2 + logging: + driver: json-file + options: + max-size: "10m" + expose: + - 389 + environment: + STORAGE_DIR: /storage/fs1 + volumes: + - ldap_data:/var/lib/openldap/openldap-data + - ldap_config:/etc/openldap/slapd.d + mariadb: + image: mariadb:10.11.4 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: secret + MYSQL_DATABASE: keycloak + MYSQL_USER: keycloak + MYSQL_PASSWORD: keycloak + volumes: + - mysql:/var/lib/mysql + keycloak: + image: dcm4che/keycloak:23.0.3 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "8843:8843" + environment: + KC_HTTPS_PORT: 8843 + KC_HOSTNAME: localhost + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: changeit + KC_DB: mariadb + KC_DB_URL_DATABASE: keycloak + KC_DB_URL_HOST: mariadb + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: keycloak + KC_LOG: file + ARCHIVE_HOST: localhost + KEYCLOAK_WAIT_FOR: ldap:389 mariadb:3306 + depends_on: + - ldap + - mariadb + volumes: + - keycloak:/opt/keycloak/data + db: + image: dcm4che/postgres-dcm4chee:15.4-31 + logging: + driver: json-file + options: + max-size: "10m" + expose: + - 5432 + environment: + POSTGRES_DB: pacsdb + POSTGRES_USER: pacs + POSTGRES_PASSWORD: pacs + volumes: + - db_data:/var/lib/postgresql/data + arc: + image: dcm4che/dcm4chee-arc-psql:5.31.2-secure + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "8008:8080" + environment: + POSTGRES_DB: pacsdb + POSTGRES_USER: pacs + POSTGRES_PASSWORD: pacs + AUTH_SERVER_URL: https://keycloak:8843 + WILDFLY_CHOWN: /opt/wildfly/standalone /storage + WILDFLY_WAIT_FOR: ldap:389 db:5432 keycloak:8843 + depends_on: + - ldap + - keycloak + - db + volumes: + - arc_data:/storage diff --git a/.circleci/dcm4chee/create_keycloak_token.py b/.circleci/dcm4chee/create_keycloak_token.py new file mode 100644 index 000000000..28a57df5a --- /dev/null +++ b/.circleci/dcm4chee/create_keycloak_token.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 + +# This script can be used to create a keycloak token for the +# dcm4chee server via the python-keycloak API. python-keycloak +# must be installed. + +from keycloak import KeycloakOpenID + +keycloack_openid = KeycloakOpenID( + server_url='https://localhost:8843', + client_id='dcm4chee-arc-rs', + realm_name='dcm4che', + client_secret_key='changeit', + # Certificate is not working, just don't verify... + verify=False, +) + +token_dict = keycloack_openid.token('user', 'changeit') +print(token_dict['access_token']) diff --git a/.circleci/dcm4chee/docker-compose.yml b/.circleci/dcm4chee/docker-compose.yml new file mode 100644 index 000000000..b81489ac9 --- /dev/null +++ b/.circleci/dcm4chee/docker-compose.yml @@ -0,0 +1,54 @@ +--- +version: "3" +volumes: + db_data: {} + arc_data: {} + ldap_data: {} + ldap_config: {} +services: + ldap: + image: dcm4che/slapd-dcm4chee:2.6.5-31.2 + logging: + driver: json-file + options: + max-size: "10m" + expose: + - 389 + environment: + STORAGE_DIR: /storage/fs1 + volumes: + - ldap_data:/var/lib/openldap/openldap-data + - ldap_config:/etc/openldap/slapd.d + db: + image: dcm4che/postgres-dcm4chee:15.4-31 + logging: + driver: json-file + options: + max-size: "10m" + expose: + - 5432 + environment: + POSTGRES_DB: pacsdb + POSTGRES_USER: pacs + POSTGRES_PASSWORD: pacs + volumes: + - db_data:/var/lib/postgresql/data + arc: + image: dcm4che/dcm4chee-arc-psql:5.31.2 + logging: + driver: json-file + options: + max-size: "10m" + ports: + - "8008:8080" + environment: + POSTGRES_DB: pacsdb + POSTGRES_USER: pacs + POSTGRES_PASSWORD: pacs + WILDFLY_CHOWN: /opt/wildfly/standalone /storage + WILDFLY_WAIT_FOR: ldap:389 db:5432 + depends_on: + - ldap + - db + volumes: + - arc_data:/storage diff --git a/.circleci/dcm4chee/update_access_token_lifespan.py b/.circleci/dcm4chee/update_access_token_lifespan.py new file mode 100644 index 000000000..c9ceb14ce --- /dev/null +++ b/.circleci/dcm4chee/update_access_token_lifespan.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +# Change the access token life span in the realm settings + +import requests +from keycloak import KeycloakOpenIDConnection + + +def create_openid_admin_token(): + # Get an admin OpenID access token. This expires after 60 seconds. + return KeycloakOpenIDConnection( + server_url='https://localhost:8843', + username='admin', + password='changeit', + realm_name='master', + verify=False, + ).token['access_token'] + + +def set_access_token_life_span(token, lifespan): + # curl command looks like this: + # curl 'https://localhost:8843/admin/realms/dcm4che' \ + # -X 'PUT' \ + # -H 'Content-Type: application/json' \ + # -H 'authorization: Bearer $TOKEN' \ + # -d '{"accessTokenLifespan":6000}' \ + # --insecure + session = requests.Session() + session.headers.update({'Authorization': f'Bearer {token}'}) + + url = 'https://localhost:8843/admin/realms/dcm4che' + r = session.put(url, json={'accessTokenLifespan': lifespan}, verify=False) + r.raise_for_status() + + +if __name__ == '__main__': + token = create_openid_admin_token() + + # Set default timetout to be 1 hour + set_access_token_life_span(token, 3600) diff --git a/.circleci/dcm4chee/upload_example_data.py b/.circleci/dcm4chee/upload_example_data.py new file mode 100644 index 000000000..80bd308aa --- /dev/null +++ b/.circleci/dcm4chee/upload_example_data.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import urllib.request +from io import BytesIO + +from dicomweb_client import DICOMwebClient +from pydicom import dcmread +from requests import Session + + +def upload_example_data(server_url, token=None): + + # This is TCGA-AA-3697 + sha512s = [ + '48cb562b94d0daf4060abd9eef150c851d3509d9abbff4bea11d00832955720bf1941073a51e6fb68fb5cc23704dec2659fc0c02360a8ac753dc523dca2c8c36', # noqa + '36432183380eb7d44417a2210a19d550527abd1181255e19ed5c1d17695d8bb8ca42f5b426a63fa73b84e0e17b770401a377ae0c705d0ed7fdf30d571ef60e2d', # noqa + '99bd3da4b8e11ce7b4f7ed8a294ed0c37437320667a06c40c383f4b29be85fe8e6094043e0600bee0ba879f2401de4c57285800a4a23da2caf2eb94e5b847ee0', # noqa + ] + download_urls = [ + f'https://data.kitware.com/api/v1/file/hashsum/sha512/{x}/download' for x in sha512s + ] + + datasets = [] + for url in download_urls: + resp = urllib.request.urlopen(url) + data = resp.read() + dataset = dcmread(BytesIO(data)) + datasets.append(dataset) + + if token is not None: + session = Session() + session.headers.update({'Authorization': f'Bearer {token}'}) + else: + session = None + + client = DICOMwebClient(server_url, session=session) + client.store_instances(datasets) + + +if __name__ == '__main__': + import os + + url = os.getenv('DICOMWEB_TEST_URL') + if url is None: + msg = 'DICOMWEB_TEST_URL must be set' + raise Exception(msg) + + token = os.getenv('DICOMWEB_TEST_TOKEN') + upload_example_data(url, token=token) diff --git a/.circleci/make_index.py b/.circleci/make_index.py new file mode 100644 index 000000000..cdb1f1ffb --- /dev/null +++ b/.circleci/make_index.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +import os +import sys +import time + +path = 'gh-pages' if len(sys.argv) == 1 else sys.argv[1] +indexName = 'index.html' +template = """ +large_image_wheels + +

large_image_wheels

+
+%LINKS%
+
+ +""" +link = '%s%s%s%11d' + +wheels = [(name, name) for name in os.listdir(path) if name.endswith('whl')] + +wheels = sorted(wheels) +maxnamelen = max(len(name) for name, url in wheels) +index = template.replace('%LINKS%', '\n'.join([ + link % ( + url, name, name, ' ' * (maxnamelen + 3 - len(name)), + time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(os.path.getmtime( + os.path.join(path, name)))), + os.path.getsize(os.path.join(path, name)), + ) for name, url in wheels])) +open(os.path.join(path, indexName), 'w').write(index) diff --git a/.circleci/make_wheels.sh b/.circleci/make_wheels.sh new file mode 100644 index 000000000..ce9251cda --- /dev/null +++ b/.circleci/make_wheels.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +set -e + +ROOTPATH=`pwd` + +pip install --user -U setuptools_scm wheel +export SETUPTOOLS_SCM_PRETEND_VERSION=`python -m setuptools_scm | sed "s/.* //"` +if [ ${CIRCLE_BRANCH-:} = "master" ]; then export SETUPTOOLS_SCM_PRETEND_VERSION=`echo $SETUPTOOLS_SCM_PRETEND_VERSION | sed "s/\+.*$//"`; fi + +mkdir ~/wheels + +# If we need binary wheels, we would need to step through the various versions +# of python and also run auditwheel on each output +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/girder" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/girder_annotation" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/utilities/converter" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/utilities/tasks" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/bioformats" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/deepzoom" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/dicom" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/dummy" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/gdal" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/mapnik" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/multi" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/nd2" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/ometiff" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/openjpeg" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/openslide" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/pil" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/rasterio" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/test" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/tiff" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/tifffile" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/vips" +pip wheel . --no-deps -w ~/wheels && rm -rf build +cd "$ROOTPATH/sources/zarr" +pip wheel . --no-deps -w ~/wheels && rm -rf build diff --git a/.circleci/release_pypi.sh b/.circleci/release_pypi.sh new file mode 100644 index 000000000..3e77ccac1 --- /dev/null +++ b/.circleci/release_pypi.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +set -e + +ROOTPATH=`pwd` + +export SETUPTOOLS_SCM_PRETEND_VERSION=`python -m setuptools_scm | sed "s/.* //"` +if [ ${CIRCLE_BRANCH-:} = "master" ]; then export SETUPTOOLS_SCM_PRETEND_VERSION=`echo $SETUPTOOLS_SCM_PRETEND_VERSION | sed "s/\+.*$//"`; fi + +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/girder" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/girder_annotation" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/utilities/converter" +# cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/utilities/tasks" +# cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/bioformats" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/deepzoom" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/dicom" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/dummy" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/gdal" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/mapnik" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/multi" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/nd2" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/ometiff" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/openjpeg" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/openslide" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/pil" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/rasterio" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/test" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/tiff" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/tifffile" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/vips" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* +cd "$ROOTPATH/sources/zarr" +cp "$ROOTPATH/README.rst" . +cp "$ROOTPATH/LICENSE" . +python setup.py sdist +pip wheel . --no-deps -w dist +twine ${1:-check} $( [[ "${1:-check}" == "upload" ]] && printf %s '--verbose' ) dist/* diff --git a/.doctrees/_build/girder_large_image/girder_large_image.doctree b/.doctrees/_build/girder_large_image/girder_large_image.doctree new file mode 100644 index 000000000..966dff2dd Binary files /dev/null and b/.doctrees/_build/girder_large_image/girder_large_image.doctree differ diff --git a/.doctrees/_build/girder_large_image/girder_large_image.models.doctree b/.doctrees/_build/girder_large_image/girder_large_image.models.doctree new file mode 100644 index 000000000..660389c76 Binary files /dev/null and b/.doctrees/_build/girder_large_image/girder_large_image.models.doctree differ diff --git a/.doctrees/_build/girder_large_image/girder_large_image.rest.doctree b/.doctrees/_build/girder_large_image/girder_large_image.rest.doctree new file mode 100644 index 000000000..b8503a877 Binary files /dev/null and b/.doctrees/_build/girder_large_image/girder_large_image.rest.doctree differ diff --git a/.doctrees/_build/girder_large_image/modules.doctree b/.doctrees/_build/girder_large_image/modules.doctree new file mode 100644 index 000000000..f61c668c2 Binary files /dev/null and b/.doctrees/_build/girder_large_image/modules.doctree differ diff --git a/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.doctree b/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.doctree new file mode 100644 index 000000000..5a63f75fc Binary files /dev/null and b/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.doctree differ diff --git a/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.models.doctree b/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.models.doctree new file mode 100644 index 000000000..c2f3966ce Binary files /dev/null and b/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.models.doctree differ diff --git a/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.rest.doctree b/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.rest.doctree new file mode 100644 index 000000000..dc3fe7292 Binary files /dev/null and b/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.rest.doctree differ diff --git a/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.utils.doctree b/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.utils.doctree new file mode 100644 index 000000000..bab2f563e Binary files /dev/null and b/.doctrees/_build/girder_large_image_annotation/girder_large_image_annotation.utils.doctree differ diff --git a/.doctrees/_build/girder_large_image_annotation/modules.doctree b/.doctrees/_build/girder_large_image_annotation/modules.doctree new file mode 100644 index 000000000..e1f214754 Binary files /dev/null and b/.doctrees/_build/girder_large_image_annotation/modules.doctree differ diff --git a/.doctrees/_build/large_image/large_image.cache_util.doctree b/.doctrees/_build/large_image/large_image.cache_util.doctree new file mode 100644 index 000000000..9d9e9163b Binary files /dev/null and b/.doctrees/_build/large_image/large_image.cache_util.doctree differ diff --git a/.doctrees/_build/large_image/large_image.doctree b/.doctrees/_build/large_image/large_image.doctree new file mode 100644 index 000000000..1646132cc Binary files /dev/null and b/.doctrees/_build/large_image/large_image.doctree differ diff --git a/.doctrees/_build/large_image/large_image.tilesource.doctree b/.doctrees/_build/large_image/large_image.tilesource.doctree new file mode 100644 index 000000000..75480dce7 Binary files /dev/null and b/.doctrees/_build/large_image/large_image.tilesource.doctree differ diff --git a/.doctrees/_build/large_image/modules.doctree b/.doctrees/_build/large_image/modules.doctree new file mode 100644 index 000000000..4d85b996a Binary files /dev/null and b/.doctrees/_build/large_image/modules.doctree differ diff --git a/.doctrees/_build/large_image_converter/large_image_converter.doctree b/.doctrees/_build/large_image_converter/large_image_converter.doctree new file mode 100644 index 000000000..0a07df83a Binary files /dev/null and b/.doctrees/_build/large_image_converter/large_image_converter.doctree differ diff --git a/.doctrees/_build/large_image_converter/modules.doctree b/.doctrees/_build/large_image_converter/modules.doctree new file mode 100644 index 000000000..8e84699f9 Binary files /dev/null and b/.doctrees/_build/large_image_converter/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_bioformats/large_image_source_bioformats.doctree b/.doctrees/_build/large_image_source_bioformats/large_image_source_bioformats.doctree new file mode 100644 index 000000000..599c87f1b Binary files /dev/null and b/.doctrees/_build/large_image_source_bioformats/large_image_source_bioformats.doctree differ diff --git a/.doctrees/_build/large_image_source_bioformats/modules.doctree b/.doctrees/_build/large_image_source_bioformats/modules.doctree new file mode 100644 index 000000000..e9fb739ce Binary files /dev/null and b/.doctrees/_build/large_image_source_bioformats/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_deepzoom/large_image_source_deepzoom.doctree b/.doctrees/_build/large_image_source_deepzoom/large_image_source_deepzoom.doctree new file mode 100644 index 000000000..f56db38bd Binary files /dev/null and b/.doctrees/_build/large_image_source_deepzoom/large_image_source_deepzoom.doctree differ diff --git a/.doctrees/_build/large_image_source_deepzoom/modules.doctree b/.doctrees/_build/large_image_source_deepzoom/modules.doctree new file mode 100644 index 000000000..ffcc225a5 Binary files /dev/null and b/.doctrees/_build/large_image_source_deepzoom/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_dicom/large_image_source_dicom.assetstore.doctree b/.doctrees/_build/large_image_source_dicom/large_image_source_dicom.assetstore.doctree new file mode 100644 index 000000000..33aa3d49f Binary files /dev/null and b/.doctrees/_build/large_image_source_dicom/large_image_source_dicom.assetstore.doctree differ diff --git a/.doctrees/_build/large_image_source_dicom/large_image_source_dicom.doctree b/.doctrees/_build/large_image_source_dicom/large_image_source_dicom.doctree new file mode 100644 index 000000000..406ecda04 Binary files /dev/null and b/.doctrees/_build/large_image_source_dicom/large_image_source_dicom.doctree differ diff --git a/.doctrees/_build/large_image_source_dicom/modules.doctree b/.doctrees/_build/large_image_source_dicom/modules.doctree new file mode 100644 index 000000000..6daf1fb52 Binary files /dev/null and b/.doctrees/_build/large_image_source_dicom/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_dummy/large_image_source_dummy.doctree b/.doctrees/_build/large_image_source_dummy/large_image_source_dummy.doctree new file mode 100644 index 000000000..20903aecf Binary files /dev/null and b/.doctrees/_build/large_image_source_dummy/large_image_source_dummy.doctree differ diff --git a/.doctrees/_build/large_image_source_dummy/modules.doctree b/.doctrees/_build/large_image_source_dummy/modules.doctree new file mode 100644 index 000000000..82b6dbef1 Binary files /dev/null and b/.doctrees/_build/large_image_source_dummy/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_gdal/large_image_source_gdal.doctree b/.doctrees/_build/large_image_source_gdal/large_image_source_gdal.doctree new file mode 100644 index 000000000..6ce6d160c Binary files /dev/null and b/.doctrees/_build/large_image_source_gdal/large_image_source_gdal.doctree differ diff --git a/.doctrees/_build/large_image_source_gdal/modules.doctree b/.doctrees/_build/large_image_source_gdal/modules.doctree new file mode 100644 index 000000000..cbc91cd65 Binary files /dev/null and b/.doctrees/_build/large_image_source_gdal/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_mapnik/large_image_source_mapnik.doctree b/.doctrees/_build/large_image_source_mapnik/large_image_source_mapnik.doctree new file mode 100644 index 000000000..cb25a70a1 Binary files /dev/null and b/.doctrees/_build/large_image_source_mapnik/large_image_source_mapnik.doctree differ diff --git a/.doctrees/_build/large_image_source_mapnik/modules.doctree b/.doctrees/_build/large_image_source_mapnik/modules.doctree new file mode 100644 index 000000000..c49076cd5 Binary files /dev/null and b/.doctrees/_build/large_image_source_mapnik/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_multi/large_image_source_multi.doctree b/.doctrees/_build/large_image_source_multi/large_image_source_multi.doctree new file mode 100644 index 000000000..b76487066 Binary files /dev/null and b/.doctrees/_build/large_image_source_multi/large_image_source_multi.doctree differ diff --git a/.doctrees/_build/large_image_source_multi/modules.doctree b/.doctrees/_build/large_image_source_multi/modules.doctree new file mode 100644 index 000000000..f3bf25007 Binary files /dev/null and b/.doctrees/_build/large_image_source_multi/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_nd2/large_image_source_nd2.doctree b/.doctrees/_build/large_image_source_nd2/large_image_source_nd2.doctree new file mode 100644 index 000000000..48b1df16f Binary files /dev/null and b/.doctrees/_build/large_image_source_nd2/large_image_source_nd2.doctree differ diff --git a/.doctrees/_build/large_image_source_nd2/modules.doctree b/.doctrees/_build/large_image_source_nd2/modules.doctree new file mode 100644 index 000000000..16934f3ac Binary files /dev/null and b/.doctrees/_build/large_image_source_nd2/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_ometiff/large_image_source_ometiff.doctree b/.doctrees/_build/large_image_source_ometiff/large_image_source_ometiff.doctree new file mode 100644 index 000000000..c759fc719 Binary files /dev/null and b/.doctrees/_build/large_image_source_ometiff/large_image_source_ometiff.doctree differ diff --git a/.doctrees/_build/large_image_source_ometiff/modules.doctree b/.doctrees/_build/large_image_source_ometiff/modules.doctree new file mode 100644 index 000000000..4c67d42fd Binary files /dev/null and b/.doctrees/_build/large_image_source_ometiff/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_openjpeg/large_image_source_openjpeg.doctree b/.doctrees/_build/large_image_source_openjpeg/large_image_source_openjpeg.doctree new file mode 100644 index 000000000..63e2b6b1a Binary files /dev/null and b/.doctrees/_build/large_image_source_openjpeg/large_image_source_openjpeg.doctree differ diff --git a/.doctrees/_build/large_image_source_openjpeg/modules.doctree b/.doctrees/_build/large_image_source_openjpeg/modules.doctree new file mode 100644 index 000000000..095aeaf25 Binary files /dev/null and b/.doctrees/_build/large_image_source_openjpeg/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_openslide/large_image_source_openslide.doctree b/.doctrees/_build/large_image_source_openslide/large_image_source_openslide.doctree new file mode 100644 index 000000000..ab48fa346 Binary files /dev/null and b/.doctrees/_build/large_image_source_openslide/large_image_source_openslide.doctree differ diff --git a/.doctrees/_build/large_image_source_openslide/modules.doctree b/.doctrees/_build/large_image_source_openslide/modules.doctree new file mode 100644 index 000000000..9f1331cf5 Binary files /dev/null and b/.doctrees/_build/large_image_source_openslide/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_pil/large_image_source_pil.doctree b/.doctrees/_build/large_image_source_pil/large_image_source_pil.doctree new file mode 100644 index 000000000..f6d2c62dc Binary files /dev/null and b/.doctrees/_build/large_image_source_pil/large_image_source_pil.doctree differ diff --git a/.doctrees/_build/large_image_source_pil/modules.doctree b/.doctrees/_build/large_image_source_pil/modules.doctree new file mode 100644 index 000000000..50e924672 Binary files /dev/null and b/.doctrees/_build/large_image_source_pil/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_rasterio/large_image_source_rasterio.doctree b/.doctrees/_build/large_image_source_rasterio/large_image_source_rasterio.doctree new file mode 100644 index 000000000..b4b04c4b9 Binary files /dev/null and b/.doctrees/_build/large_image_source_rasterio/large_image_source_rasterio.doctree differ diff --git a/.doctrees/_build/large_image_source_rasterio/modules.doctree b/.doctrees/_build/large_image_source_rasterio/modules.doctree new file mode 100644 index 000000000..af4d085e1 Binary files /dev/null and b/.doctrees/_build/large_image_source_rasterio/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_test/large_image_source_test.doctree b/.doctrees/_build/large_image_source_test/large_image_source_test.doctree new file mode 100644 index 000000000..91a1d6469 Binary files /dev/null and b/.doctrees/_build/large_image_source_test/large_image_source_test.doctree differ diff --git a/.doctrees/_build/large_image_source_test/modules.doctree b/.doctrees/_build/large_image_source_test/modules.doctree new file mode 100644 index 000000000..4afbc0575 Binary files /dev/null and b/.doctrees/_build/large_image_source_test/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_tiff/large_image_source_tiff.doctree b/.doctrees/_build/large_image_source_tiff/large_image_source_tiff.doctree new file mode 100644 index 000000000..a11cce5a3 Binary files /dev/null and b/.doctrees/_build/large_image_source_tiff/large_image_source_tiff.doctree differ diff --git a/.doctrees/_build/large_image_source_tiff/modules.doctree b/.doctrees/_build/large_image_source_tiff/modules.doctree new file mode 100644 index 000000000..2b43d3222 Binary files /dev/null and b/.doctrees/_build/large_image_source_tiff/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_tifffile/large_image_source_tifffile.doctree b/.doctrees/_build/large_image_source_tifffile/large_image_source_tifffile.doctree new file mode 100644 index 000000000..ff0371b1e Binary files /dev/null and b/.doctrees/_build/large_image_source_tifffile/large_image_source_tifffile.doctree differ diff --git a/.doctrees/_build/large_image_source_tifffile/modules.doctree b/.doctrees/_build/large_image_source_tifffile/modules.doctree new file mode 100644 index 000000000..e9ab9b7e0 Binary files /dev/null and b/.doctrees/_build/large_image_source_tifffile/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_vips/large_image_source_vips.doctree b/.doctrees/_build/large_image_source_vips/large_image_source_vips.doctree new file mode 100644 index 000000000..68548ea50 Binary files /dev/null and b/.doctrees/_build/large_image_source_vips/large_image_source_vips.doctree differ diff --git a/.doctrees/_build/large_image_source_vips/modules.doctree b/.doctrees/_build/large_image_source_vips/modules.doctree new file mode 100644 index 000000000..2832a5b08 Binary files /dev/null and b/.doctrees/_build/large_image_source_vips/modules.doctree differ diff --git a/.doctrees/_build/large_image_source_zarr/large_image_source_zarr.doctree b/.doctrees/_build/large_image_source_zarr/large_image_source_zarr.doctree new file mode 100644 index 000000000..7b5142813 Binary files /dev/null and b/.doctrees/_build/large_image_source_zarr/large_image_source_zarr.doctree differ diff --git a/.doctrees/_build/large_image_source_zarr/modules.doctree b/.doctrees/_build/large_image_source_zarr/modules.doctree new file mode 100644 index 000000000..f23d13f7e Binary files /dev/null and b/.doctrees/_build/large_image_source_zarr/modules.doctree differ diff --git a/.doctrees/_build/large_image_tasks/large_image_tasks.doctree b/.doctrees/_build/large_image_tasks/large_image_tasks.doctree new file mode 100644 index 000000000..87ce15252 Binary files /dev/null and b/.doctrees/_build/large_image_tasks/large_image_tasks.doctree differ diff --git a/.doctrees/_build/large_image_tasks/modules.doctree b/.doctrees/_build/large_image_tasks/modules.doctree new file mode 100644 index 000000000..a0d4f2cde Binary files /dev/null and b/.doctrees/_build/large_image_tasks/modules.doctree differ diff --git a/.doctrees/annotations.doctree b/.doctrees/annotations.doctree new file mode 100644 index 000000000..9673f9d1a Binary files /dev/null and b/.doctrees/annotations.doctree differ diff --git a/.doctrees/api_index.doctree b/.doctrees/api_index.doctree new file mode 100644 index 000000000..5aaafb83d Binary files /dev/null and b/.doctrees/api_index.doctree differ diff --git a/.doctrees/caching.doctree b/.doctrees/caching.doctree new file mode 100644 index 000000000..2ac962045 Binary files /dev/null and b/.doctrees/caching.doctree differ diff --git a/.doctrees/config_options.doctree b/.doctrees/config_options.doctree new file mode 100644 index 000000000..2046e5936 Binary files /dev/null and b/.doctrees/config_options.doctree differ diff --git a/.doctrees/development.doctree b/.doctrees/development.doctree new file mode 100644 index 000000000..e14f60d3d Binary files /dev/null and b/.doctrees/development.doctree differ diff --git a/.doctrees/dicomweb_assetstore.doctree b/.doctrees/dicomweb_assetstore.doctree new file mode 100644 index 000000000..e17328760 Binary files /dev/null and b/.doctrees/dicomweb_assetstore.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle new file mode 100644 index 000000000..fc0af5005 Binary files /dev/null and b/.doctrees/environment.pickle differ diff --git a/.doctrees/formats.doctree b/.doctrees/formats.doctree new file mode 100644 index 000000000..e890cb49f Binary files /dev/null and b/.doctrees/formats.doctree differ diff --git a/.doctrees/getting_started.doctree b/.doctrees/getting_started.doctree new file mode 100644 index 000000000..1ea59c4ae Binary files /dev/null and b/.doctrees/getting_started.doctree differ diff --git a/.doctrees/girder_annotation_config_options.doctree b/.doctrees/girder_annotation_config_options.doctree new file mode 100644 index 000000000..0963a2cc4 Binary files /dev/null and b/.doctrees/girder_annotation_config_options.doctree differ diff --git a/.doctrees/girder_caching.doctree b/.doctrees/girder_caching.doctree new file mode 100644 index 000000000..ab8ffe24a Binary files /dev/null and b/.doctrees/girder_caching.doctree differ diff --git a/.doctrees/girder_config_options.doctree b/.doctrees/girder_config_options.doctree new file mode 100644 index 000000000..c77285609 Binary files /dev/null and b/.doctrees/girder_config_options.doctree differ diff --git a/.doctrees/girder_index.doctree b/.doctrees/girder_index.doctree new file mode 100644 index 000000000..b57cb701d Binary files /dev/null and b/.doctrees/girder_index.doctree differ diff --git a/.doctrees/image_conversion.doctree b/.doctrees/image_conversion.doctree new file mode 100644 index 000000000..57fb9b49e Binary files /dev/null and b/.doctrees/image_conversion.doctree differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree new file mode 100644 index 000000000..865da44c4 Binary files /dev/null and b/.doctrees/index.doctree differ diff --git a/.doctrees/multi_source_specification.doctree b/.doctrees/multi_source_specification.doctree new file mode 100644 index 000000000..5553af49e Binary files /dev/null and b/.doctrees/multi_source_specification.doctree differ diff --git a/.doctrees/nbsphinx/notebooks/large_image_examples.ipynb b/.doctrees/nbsphinx/notebooks/large_image_examples.ipynb new file mode 100644 index 000000000..34a2577e3 --- /dev/null +++ b/.doctrees/nbsphinx/notebooks/large_image_examples.ipynb @@ -0,0 +1,866 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "73529f76-83b2-4d2c-b8f3-01bd9c1696af", + "metadata": {}, + "source": [ + "Using Large Image in Jupyter\n", + "============================\n", + "\n", + "The large_image library has some convenience features for use in Jupyter Notebooks and Jupyter Lab. Different features are available depending on whether your data files are local or on a Girder server." + ] + }, + { + "cell_type": "markdown", + "id": "ffb9e79e-2d89-4e41-92cb-ee736833d309", + "metadata": {}, + "source": [ + "Installation\n", + "------------\n", + "\n", + "The large_image library has a variety of tile sources to support a wide range of file formats. Many of these depend\n", + "on binary libraries. For linux systems, you can install these from python wheels via the `--find-links` option. For\n", + "other operating systems, you will need to install different libraries depending on what tile sources you wish to use." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fa38be1a-341a-4725-98f0-b61318fc696a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in links: https://girder.github.io/large_image_wheels\n" + ] + } + ], + "source": [ + "# This will install large_image, including all sources and many other options\n", + "!pip install large_image[all] --find-links https://girder.github.io/large_image_wheels\n", + "# For a smaller set of tile sources, you could also do:\n", + "# !pip install large_image[pil,rasterio,tifffile]\n", + "\n", + "# For maximum capabilities in Jupyter, also install ipyleaflet so you can\n", + "# view zoomable images in the notebook\n", + "!pip install ipyleaflet\n", + "\n", + "# If you are accessing files on a Girder server, it is useful to install girder_client\n", + "!pip install girder_client" + ] + }, + { + "cell_type": "markdown", + "id": "c9a14ff3-4c28-49af-ad71-565f420770c9", + "metadata": {}, + "source": [ + "Using Local Files\n", + "-----------------\n", + "\n", + "When using large_image with local files, when you open a file, large_image returns a tile source. See [girder.github.io/large_image](https://girder.github.io/large_image) for documentation on what you can do with this.\n", + "\n", + "First, we download a few files so we can use them locally." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "73409e8c-08b3-4891-a7fc-c5c42e453ffb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " % Total % Received % Xferd Average Speed Time Time Time Current\n", + " Dload Upload Total Spent Left Speed\n", + "100 32.8M 100 32.8M 0 0 103M 0 --:--:-- --:--:-- --:--:-- 103M\n", + " % Total % Received % Xferd Average Speed Time Time Time Current\n", + " Dload Upload Total Spent Left Speed\n", + "100 59.0M 100 59.0M 0 0 96.9M 0 --:--:-- --:--:-- --:--:-- 96.8M\n" + ] + } + ], + "source": [ + "# Get a few files so we can use them locally\n", + "!curl -L -C - -o TC_NG_SFBay_US_Geo_COG.tif https://data.kitware.com/api/v1/file/hashsum/sha512/5e56cdb8fb1a02615698a153862c10d5292b1ad42836a6e8bce5627e93a387dc0d3c9b6cfbd539796500bc2d3e23eafd07550f8c214e9348880bbbc6b3b0ea0c/download\n", + "!curl -L -C - -o TCGA-AA-A02O-11A-01-BS1.svs https://data.kitware.com/api/v1/file/hashsum/sha512/1b75a4ec911017aef5c885760a3c6575dacf5f8efb59fb0e011108dce85b1f4e97b8d358f3363c1f5ea6f1c3698f037554aec1620bbdd4cac54e3d5c9c1da1fd/download" + ] + }, + { + "cell_type": "markdown", + "id": "09722713-e1e8-4ae2-939d-e9aa996e4c42", + "metadata": {}, + "source": [ + "Basic Use\n", + "---------\n", + "The large_image library has a variety of tile sources that support a wide range of formats.\n", + "In general, you don't need to know the format of a file, you can just open it.\n", + "\n", + "Every file has a common interface regardless of its format. The metadata gives a common summary of the data." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "525e98e6-103b-4b95-becc-c23931f17873", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "ImageBytes<5146> (image/jpeg)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import large_image\n", + "\n", + "ts = large_image.open('TCGA-AA-A02O-11A-01-BS1.svs')\n", + "# The thumbnail method returns a tuple with an image or numpy array and a mime type\n", + "ts.getThumbnail()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6e3ee887-a21f-426b-b221-9e3504d75870", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "bandCount": 4, + "dtype": "uint8", + "levels": 9, + "magnification": 20, + "mm_x": 0.0004991, + "mm_y": 0.0004991, + "sizeX": 55988, + "sizeY": 16256, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'levels': 9,\n", + " 'sizeX': 55988,\n", + " 'sizeY': 16256,\n", + " 'tileWidth': 256,\n", + " 'tileHeight': 256,\n", + " 'magnification': 20.0,\n", + " 'mm_x': 0.0004991,\n", + " 'mm_y': 0.0004991,\n", + " 'dtype': 'uint8',\n", + " 'bandCount': 4}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Every image's dimensions are in `sizeX` and `sizeY`. If known, a variety of other information\n", + "# is provided.\n", + "ts.metadata" + ] + }, + { + "cell_type": "markdown", + "id": "27c92320-3c21-40a0-89e0-266ee6850c4c", + "metadata": {}, + "source": [ + "If you have ipyleaflet installed and are using JupyterLab, you can ask the system to proxy requests\n", + "to an internal tile server that allows you to view the image in a zoomable viewer. There are more options\n", + "depending on your Jupyter configuration and whether it is running locally or remotely. \n", + "Some environments need different proxy options, like Google CoLab.\n", + "\n", + "If ipyleaflet isn't installed, inspecting a tile source will just show the thumbnail." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c0b16fe7-5237-4fdb-9bd4-9b017c7abc8c", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "48f48c57d0454472bf135cf1fac22cea", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[8128.0, 27994.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Ask JupyterLab to locally proxy an internal tile server\n", + "import importlib.util\n", + "\n", + "if importlib.util.find_spec('google') and importlib.util.find_spec('google.colab'):\n", + " # colab intercepts localhost\n", + " large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = 'https://localhost'\n", + "else:\n", + " large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = True\n", + "\n", + "# Look at our tile source\n", + "ts" + ] + }, + { + "cell_type": "markdown", + "id": "565cd319-7a07-4fe4-9160-b4ec84671821", + "metadata": {}, + "source": [ + "If you see a black border on the right and bottom, this is because the ipyleaflet viewer shows areas\n", + "outside the bounds of the image. We could ask for the image to be served using PNG images so that those\n", + "areas are transparent" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "25a0538f-8bfb-4079-843e-ba7732d5103c", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3b6a54aae908468880216fee266860f4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[8128.0, 27994.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ts = large_image.open('TCGA-AA-A02O-11A-01-BS1.svs', encoding='PNG')\n", + "ts" + ] + }, + { + "cell_type": "markdown", + "id": "79d07b59-05da-41f7-89ac-584e057825bf", + "metadata": {}, + "source": [ + "The IPyLeaflet map uses a bottom-up y, x coordinate system, not the top-down x, y coordinate system \n", + "most image system use. The rationale is that this is appropriate for geospatial maps with\n", + "latitude and longitude, but it doesn't carry over to pixel coordinates very well. There are some\n", + "convenience functions to convert coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0a1f6720-e4fc-47ea-8d35-158a25516b9f", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3b6a54aae908468880216fee266860f4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(bottom=232.0, center=[8128.0, 27994.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_i…" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import ipyleaflet\n", + "\n", + "# Get a reference to the IPyLeaflet Map\n", + "map = ts.iplmap\n", + "# to_map converts pixel coordinates to IPyLeaflet map coordinates.\n", + "# draw a rectangle that is wider than tall.\n", + "rectangle = ipyleaflet.Rectangle(bounds=(ts.to_map((0, 0)), ts.to_map((10000, 5000))))\n", + "map.add_layer(rectangle)\n", + "# draw another rectangle that is the size of the whole image.\n", + "rectangle = ipyleaflet.Rectangle(bounds=(ts.to_map((0, 0)), ts.to_map((ts.sizeX, ts.sizeY))))\n", + "map.add_layer(rectangle)\n", + "# show the map\n", + "map" + ] + }, + { + "cell_type": "markdown", + "id": "510883e6-2182-4959-852f-86357816ad57", + "metadata": {}, + "source": [ + "Geospatial Sources\n", + "------------------\n", + "\n", + "For geospatial sources, the default viewer shows the image in context on a world map if an appropriate projection is used." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "81556073-6db9-41f8-aa9f-1757845aedf2", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5150c395482d40fbb80df6cea8fcc4ea", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[37.752214941926994, -122.41877581711466], controls=(ZoomControl(options=['position', 'zoom_in_text…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "geots = large_image.open('TC_NG_SFBay_US_Geo_COG.tif', projection='EPSG:3857', encoding='PNG')\n", + "geots" + ] + }, + { + "cell_type": "markdown", + "id": "c58bf0a5-dfc7-4e5e-bfc0-e243acfb6313", + "metadata": {}, + "source": [ + "Geospatial sources have additional metadata and thumbnails." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5378671a-1374-4f42-822c-94f89cbaa267", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "bandCount": 3, + "bands": { + "1": { + "interpretation": "red", + "max": 255, + "mean": 56.164648651261, + "min": 5, + "stdev": 45.505628098154 + }, + "2": { + "interpretation": "green", + "max": 255, + "mean": 61.590676043792, + "min": 2, + "stdev": 35.532493975171 + }, + "3": { + "interpretation": "blue", + "max": 255, + "mean": 47.00898008224, + "min": 1, + "stdev": 29.470217162239 + } + }, + "bounds": { + "ll": { + "x": -13660993.43811085, + "y": 4502326.297712617 + }, + "lr": { + "x": -13594198.136883384, + "y": 4502326.297712617 + }, + "srs": "epsg:3857", + "ul": { + "x": -13660993.43811085, + "y": 4586806.951318035 + }, + "ur": { + "x": -13594198.136883384, + "y": 4586806.951318035 + }, + "xmax": -13594198.136883384, + "xmin": -13660993.43811085, + "ymax": 4586806.951318035, + "ymin": 4502326.297712617 + }, + "dtype": "uint8", + "geospatial": true, + "levels": 15, + "magnification": null, + "mm_x": 1381.876143450579, + "mm_y": 1381.876143450579, + "projection": "epsg:3857", + "sizeX": 4194304, + "sizeY": 4194304, + "sourceBounds": { + "ll": { + "x": -122.71879201711468, + "y": 37.45219874192699 + }, + "lr": { + "x": -122.11875961711466, + "y": 37.45219874192699 + }, + "srs": "+proj=longlat +datum=WGS84 +no_defs", + "ul": { + "x": -122.71879201711468, + "y": 38.052231141926995 + }, + "ur": { + "x": -122.11875961711466, + "y": 38.052231141926995 + }, + "xmax": -122.11875961711466, + "xmin": -122.71879201711468, + "ymax": 38.052231141926995, + "ymin": 37.45219874192699 + }, + "sourceLevels": 6, + "sourceSizeX": 4323, + "sourceSizeY": 4323, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'levels': 15,\n", + " 'sizeX': 4194304,\n", + " 'sizeY': 4194304,\n", + " 'tileWidth': 256,\n", + " 'tileHeight': 256,\n", + " 'magnification': None,\n", + " 'mm_x': 1381.876143450579,\n", + " 'mm_y': 1381.876143450579,\n", + " 'dtype': 'uint8',\n", + " 'bandCount': 3,\n", + " 'geospatial': True,\n", + " 'sourceLevels': 6,\n", + " 'sourceSizeX': 4323,\n", + " 'sourceSizeY': 4323,\n", + " 'bounds': {'ll': {'x': -13660993.43811085, 'y': 4502326.297712617},\n", + " 'ul': {'x': -13660993.43811085, 'y': 4586806.951318035},\n", + " 'lr': {'x': -13594198.136883384, 'y': 4502326.297712617},\n", + " 'ur': {'x': -13594198.136883384, 'y': 4586806.951318035},\n", + " 'srs': 'epsg:3857',\n", + " 'xmin': -13660993.43811085,\n", + " 'xmax': -13594198.136883384,\n", + " 'ymin': 4502326.297712617,\n", + " 'ymax': 4586806.951318035},\n", + " 'projection': 'epsg:3857',\n", + " 'sourceBounds': {'ll': {'x': -122.71879201711467, 'y': 37.45219874192699},\n", + " 'ul': {'x': -122.71879201711467, 'y': 38.052231141926995},\n", + " 'lr': {'x': -122.11875961711466, 'y': 37.45219874192699},\n", + " 'ur': {'x': -122.11875961711466, 'y': 38.052231141926995},\n", + " 'srs': '+proj=longlat +datum=WGS84 +no_defs',\n", + " 'xmin': -122.71879201711467,\n", + " 'xmax': -122.11875961711466,\n", + " 'ymin': 37.45219874192699,\n", + " 'ymax': 38.052231141926995},\n", + " 'bands': {1: {'min': 5.0,\n", + " 'max': 255.0,\n", + " 'mean': 56.164648651261,\n", + " 'stdev': 45.505628098154,\n", + " 'interpretation': 'red'},\n", + " 2: {'min': 2.0,\n", + " 'max': 255.0,\n", + " 'mean': 61.590676043792,\n", + " 'stdev': 35.532493975171,\n", + " 'interpretation': 'green'},\n", + " 3: {'min': 1.0,\n", + " 'max': 255.0,\n", + " 'mean': 47.00898008224,\n", + " 'stdev': 29.470217162239,\n", + " 'interpretation': 'blue'}}}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "geots.metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e89565cb-bbe3-4958-a691-f24aa538083d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "ImageBytes<33608> (image/jpeg)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "geots.getThumbnail()[0]" + ] + }, + { + "cell_type": "markdown", + "id": "f236bd95-fd77-4d37-9749-004f40fb8470", + "metadata": {}, + "source": [ + "Girder Server Sources\n", + "---------------------\n", + "\n", + "You can use files on a Girder server by just download them and using them locally.\n", + "However, you can use girder client to access files more conveniently. If the Girder server\n", + "doesn't have the large_image plugin installed on it, this can still be useful -- functionally,\n", + "this pulls the file and provides a local tile server, so some of this requires the same\n", + "proxy setup as a local file.\n", + "\n", + "`large_image.tilesource.jupyter.Map` is a convenience class that can use a variety of remote sources.\n", + "\n", + "**(1)** We can get a source from girder via item or file id" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "9ebe43fb-affa-43ab-be42-064bd75bcbf7", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "04fae714b34f46be91a859c8dc0c9768", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[6917.5, 15936.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import girder_client\n", + "\n", + "gc1 = girder_client.GirderClient(apiUrl='https://data.kitware.com/api/v1')\n", + "# If you need to authenticate, an easy way is to ask directly\n", + "# gc.authenticate(interactive=True)\n", + "# but you could also use an API token or a variety of other methods.\n", + "\n", + "# We can ask for the image by item or file id\n", + "map1 = large_image.tilesource.jupyter.Map(gc=gc1, id='57b345d28d777f126827dc28')\n", + "map1" + ] + }, + { + "cell_type": "markdown", + "id": "707114c4-2cd0-4d86-a41d-21105a8761b7", + "metadata": {}, + "source": [ + "**(2)** We could use a resource path instead of an id" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a28637e4-5c34-4b59-8618-5c9e7908b00c", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "554b0d4fa33545e7992e86976de34e02", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[5636.5, 4579.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "map2 = large_image.tilesource.jupyter.Map(gc=gc1, resource='/collection/HistomicsTK/CI and tox Test Data/large_image test files/Huron.Image2_JPEG2K.tif')\n", + "map2" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1ea9cdae-57b1-4708-8f9e-41da933b90e2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'5818e9418d777f10f26ee443'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# You can get an id of an item using pure girder client calls, too. For instance, internally, the\n", + "# id is fetched from the resource path and then used.\n", + "resourceFromMap2 = '/collection/HistomicsTK/CI and tox Test Data/large_image test files/Huron.Image2_JPEG2K.tif'\n", + "idOfResource = gc1.get('resource/lookup', parameters={'path': resourceFromMap2})['_id']\n", + "idOfResource" + ] + }, + { + "cell_type": "markdown", + "id": "535a3990-62e1-4063-bc5f-edf5494b114f", + "metadata": {}, + "source": [ + "**(3)** We can use a girder server that has the large_image plugin enabled. This lets us do more than\n", + "just look at the image." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "b9611e09", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "aec5a5161aad4ebf9273d13ccdcc4dd5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[45252.0, 54717.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gc2 = girder_client.GirderClient(apiUrl='https://demo.kitware.com/histomicstk/api/v1')\n", + "\n", + "resourcePath = '/collection/Crowd Source Paper/All slides/TCGA-A1-A0SP-01Z-00-DX1.20D689C6-EFA5-4694-BE76-24475A89ACC0.svs'\n", + "map3 = large_image.tilesource.jupyter.Map(gc=gc2, resource=resourcePath)\n", + "map3" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "48d263ec-e350-43f4-9b2f-0c7bfb508e02", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "dtype": "uint8", + "levels": 10, + "magnification": 40, + "mm_x": 0.0002521, + "mm_y": 0.0002521, + "sizeX": 109434, + "sizeY": 90504, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'dtype': 'uint8',\n", + " 'levels': 10,\n", + " 'magnification': 40.0,\n", + " 'mm_x': 0.0002521,\n", + " 'mm_y': 0.0002521,\n", + " 'sizeX': 109434,\n", + " 'sizeY': 90504,\n", + " 'tileHeight': 256,\n", + " 'tileWidth': 256}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We can check the metadata\n", + "map3.metadata" + ] + }, + { + "cell_type": "markdown", + "id": "3ade5165-4628-4e5d-a601-6dd70fcb9190", + "metadata": {}, + "source": [ + "We can get data as a numpy array." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d5ad935a-3cef-41cf-95ed-3a8b79679b93", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[240, 242, 241, 255],\n", + " [240, 242, 241, 255],\n", + " [241, 242, 242, 255],\n", + " ...,\n", + " [238, 240, 239, 253],\n", + " [239, 241, 240, 255],\n", + " [239, 241, 240, 255]],\n", + "\n", + " [[240, 241, 240, 255],\n", + " [239, 241, 240, 255],\n", + " [240, 241, 240, 255],\n", + " ...,\n", + " [237, 238, 238, 253],\n", + " [237, 239, 238, 255],\n", + " [237, 239, 238, 255]],\n", + "\n", + " [[239, 241, 240, 255],\n", + " [239, 241, 240, 255],\n", + " [239, 241, 240, 255],\n", + " ...,\n", + " [236, 238, 237, 253],\n", + " [237, 239, 238, 255],\n", + " [237, 239, 238, 255]],\n", + "\n", + " ...,\n", + "\n", + " [[240, 241, 241, 255],\n", + " [240, 241, 241, 255],\n", + " [240, 241, 241, 255],\n", + " ...,\n", + " [239, 240, 239, 253],\n", + " [240, 241, 240, 255],\n", + " [239, 241, 240, 255]],\n", + "\n", + " [[241, 243, 242, 255],\n", + " [241, 242, 242, 255],\n", + " [241, 242, 242, 255],\n", + " ...,\n", + " [238, 241, 240, 253],\n", + " [239, 242, 241, 255],\n", + " [239, 241, 241, 255]],\n", + "\n", + " [[237, 239, 240, 253],\n", + " [237, 240, 240, 253],\n", + " [236, 239, 239, 253],\n", + " ...,\n", + " [234, 237, 238, 251],\n", + " [234, 237, 237, 253],\n", + " [235, 238, 238, 253]]], dtype=uint8)" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pickle\n", + "\n", + "pickle.loads(gc2.get(f'item/{map3.id}/tiles/region', parameters={'encoding': 'pickle', 'width': 100, 'height': 100}, jsonResp=False).content)\n" + ] + }, + { + "cell_type": "markdown", + "id": "a5e0f551-eb64-4a1b-b28a-d2c54854fab8", + "metadata": {}, + "source": [ + "**(4)** From a metadata dictionary and a url. Any slippy-map style tile server could be used." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ef8e1818-cafc-4e0a-bf62-a7d2b6d8f453", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "785dccbcaeb54d6d9921acf13aca24fd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[38436.5, 47879.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# There can be additional items in the metadata, but this is minimum required.\n", + "remoteMetadata = {\n", + " 'levels': 10,\n", + " 'sizeX': 95758,\n", + " 'sizeY': 76873,\n", + " 'tileHeight': 256,\n", + " 'tileWidth': 256,\n", + "}\n", + "remoteUrl = 'https://demo.kitware.com/histomicstk/api/v1/item/5bbdeec6e629140048d01bb9/tiles/zxy/{z}/{x}/{y}?encoding=PNG'\n", + "\n", + "map4 = large_image.tilesource.jupyter.Map(metadata=remoteMetadata, url=remoteUrl)\n", + "map4" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/notebooks/zarr_sink_example.ipynb b/.doctrees/nbsphinx/notebooks/zarr_sink_example.ipynb new file mode 100644 index 000000000..67ce180d0 --- /dev/null +++ b/.doctrees/nbsphinx/notebooks/zarr_sink_example.ipynb @@ -0,0 +1,561 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9190dbbd", + "metadata": {}, + "source": [ + "# Using the Zarr Tile Sink\n", + "\n", + "The `ZarrFileTileSource` class has file-writing capabilities; an empty image can be created, image data can be added as tiles or arbitrary regions, and the image can be saved to a file in any of several formats.\n", + "\n", + "Typically, this class is called a \"source\" when reading from a file and a \"sink\" when writing to a file. This is just a naming convention, but the read mode and write mode are not mutually exclusive." + ] + }, + { + "cell_type": "markdown", + "id": "ebaa9c80", + "metadata": {}, + "source": [ + "## Installation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ba28d02", + "metadata": {}, + "outputs": [], + "source": [ + "# This will install large_image with the zarr source\n", + "!pip install large_image[tiff,zarr,converter] --find-links https://girder.github.io/large_image_wheels\n", + "\n", + "# For maximum capabilities in Jupyter, also install ipyleaflet so you can\n", + "# view zoomable images in the notebook\n", + "!pip install ipyleaflet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63c0c38f", + "metadata": {}, + "outputs": [], + "source": [ + "# Ask JupyterLab to locally proxy an internal tile server\n", + "import importlib.util\n", + "import large_image\n", + "\n", + "if importlib.util.find_spec('google') and importlib.util.find_spec('google.colab'):\n", + " # colab intercepts localhost\n", + " large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = 'https://localhost'\n", + "else:\n", + " large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = True" + ] + }, + { + "cell_type": "markdown", + "id": "771078f9", + "metadata": {}, + "source": [ + "## Sample Data Download\n", + "\n", + "For this example, we will use data from a sample file. We will copy and modify tiles from this image, writing the modified data to a new file." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1c4b746b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " % Total % Received % Xferd Average Speed Time Time Time Current\n", + " Dload Upload Total Spent Left Speed\n", + "100 12.3M 100 12.3M 0 0 2952k 0 0:00:04 0:00:04 --:--:-- 2952k\n" + ] + } + ], + "source": [ + "!curl -L -C - -o example.tiff https://demo.kitware.com/histomicstk/api/v1/item/58b480ba92ca9a000b08c899/download" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5c39f222", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "bandCount": 3, + "dtype": "uint8", + "levels": 7, + "magnification": 40, + "mm_x": 0.00025, + "mm_y": 0.00025, + "sizeX": 9216, + "sizeY": 11264, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'levels': 7,\n", + " 'sizeX': 9216,\n", + " 'sizeY': 11264,\n", + " 'tileWidth': 256,\n", + " 'tileHeight': 256,\n", + " 'magnification': 40.0,\n", + " 'mm_x': 0.00025,\n", + " 'mm_y': 0.00025,\n", + " 'dtype': 'uint8',\n", + " 'bandCount': 3}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "original_image_path = 'example.tiff'\n", + "processed_image_path = 'processed_example_1.tiff'\n", + "\n", + "source = large_image.open(original_image_path)\n", + "\n", + "# view the metadata\n", + "source_metadata = source.getMetadata()\n", + "source_metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d2d4956c", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "ImageBytes<12955> (image/jpeg)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# show source as a static thumbnail\n", + "source.getThumbnail()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c018cebd", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bf6d0e9480cb43ef9851d7bd3ca7e356", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Map(center=[5632.0, 4608.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_i…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# show the source image in an interactive viewer\n", + "source" + ] + }, + { + "cell_type": "markdown", + "id": "f88191b2", + "metadata": {}, + "source": [ + "## Writing Processed Data to a New File" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e75e6de", + "metadata": {}, + "outputs": [], + "source": [ + "from skimage.color.adapt_rgb import adapt_rgb, hsv_value\n", + "from skimage import filters\n", + "\n", + "# define some image processing function\n", + "\n", + "@adapt_rgb(hsv_value)\n", + "def process_tile(tile, footprint_size):\n", + " return filters.unsharp_mask(\n", + " tile, radius=footprint_size,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "fb89a0b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Processing image for 3 frames.\n", + "Processing image with footprint_size = 1\n", + "Processing image with footprint_size = 10\n", + "Processing image with footprint_size = 50\n" + ] + }, + { + "data": { + "application/json": { + "IndexRange": { + "IndexI": 3 + }, + "IndexStride": { + "IndexI": 1 + }, + "bandCount": 3, + "channelmap": { + "Band 1": 0 + }, + "channels": [ + "Band 1" + ], + "dtype": "float64", + "frames": [ + { + "Channel": "Band 1", + "Frame": 0, + "Index": 0, + "IndexI": 0 + }, + { + "Channel": "Band 1", + "Frame": 1, + "Index": 1, + "IndexI": 1 + }, + { + "Channel": "Band 1", + "Frame": 2, + "Index": 2, + "IndexI": 2 + } + ], + "levels": 6, + "magnification": null, + "mm_x": 0, + "mm_y": 0, + "sizeX": 9216, + "sizeY": 11264, + "tileHeight": 512, + "tileWidth": 512 + }, + "text/plain": [ + "{'levels': 6,\n", + " 'sizeX': 9216,\n", + " 'sizeY': 11264,\n", + " 'tileWidth': 512,\n", + " 'tileHeight': 512,\n", + " 'magnification': None,\n", + " 'mm_x': 0,\n", + " 'mm_y': 0,\n", + " 'dtype': 'float64',\n", + " 'bandCount': 3,\n", + " 'frames': [{'Frame': 0, 'IndexI': 0, 'Index': 0, 'Channel': 'Band 1'},\n", + " {'Frame': 1, 'IndexI': 1, 'Index': 1, 'Channel': 'Band 1'},\n", + " {'Frame': 2, 'IndexI': 2, 'Index': 2, 'Channel': 'Band 1'}],\n", + " 'IndexRange': {'IndexI': 3},\n", + " 'IndexStride': {'IndexI': 1},\n", + " 'channels': ['Band 1'],\n", + " 'channelmap': {'Band 1': 0}}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# create a sink, which is an instance of ZarrFileTileSource and has no data\n", + "sink = large_image.new()\n", + "\n", + "# compare three different footprint sizes for processing algorithm\n", + "# computing the processed image takes about 1 minute for each value\n", + "footprint_sizes = [1, 10, 50]\n", + "print(f'Processing image for {len(footprint_sizes)} frames.')\n", + "\n", + "# create a frame for each processed result\n", + "for i, footprint_size in enumerate(footprint_sizes):\n", + " print('Processing image with footprint_size = ', footprint_size)\n", + " # iterate through tiles, getting numpy arrays for each tile\n", + " for tile in source.tileIterator(format='numpy'):\n", + " # for each tile, run some processing algorithm\n", + " t = tile['tile']\n", + " processed_tile = process_tile(t, footprint_size) * 255\n", + "\n", + " # add modified tile to sink\n", + " # specify tile x, tile y, and any arbitrary frame parameters\n", + " sink.addTile(processed_tile, x=tile['x'], y=tile['y'], i=i)\n", + "# view metadata\n", + "sink.getMetadata()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e9114da0", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9b8e6175005d4af89cb4cfada7b72983", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(IntSlider(value=0, description='Frame:', max=2), Map(center=[5632.0, 4608.0], controls=(ZoomCon…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# show the result image in an interactive viewer\n", + "# the viewer includes a slider for this multiframe image\n", + "# switch between frames to view the differences between the values passed to footprint_size\n", + "sink" + ] + }, + { + "cell_type": "markdown", + "id": "3c88d352", + "metadata": {}, + "source": [ + "## Edit Attributes and Write Result File" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "24e5c87c", + "metadata": {}, + "outputs": [], + "source": [ + "# set crop bounds\n", + "sink.crop = (3000, 5000, 2048, 2048)\n", + "\n", + "# set mm_x and mm_y from source metadata\n", + "sink.mm_x = source_metadata.get('mm_x')\n", + "sink.mm_y = source_metadata.get('mm_y')\n", + "\n", + "# set image description\n", + "sink.imageDescription = 'processed with scikit-image'\n", + "\n", + "# add original thumbnail as an associated image\n", + "sink.addAssociatedImage(source.getThumbnail()[0], imageKey='original')\n", + "\n", + "# write new image as tiff (other format options include .zip, .zarr, .db, .sqlite, .svs, etc.)\n", + "sink.write(processed_image_path)" + ] + }, + { + "cell_type": "markdown", + "id": "5ba5a838", + "metadata": {}, + "source": [ + "## View Results" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "46d62547", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "IndexRange": { + "IndexI": 3 + }, + "IndexStride": { + "IndexI": 1 + }, + "bandCount": 3, + "channelmap": { + "Band 1": 0 + }, + "channels": [ + "Band 1" + ], + "dtype": "uint16", + "frames": [ + { + "Channel": "Band 1", + "Frame": 0, + "Index": 0, + "IndexI": 0 + }, + { + "Channel": "Band 1", + "Frame": 1, + "Index": 1, + "IndexI": 1 + }, + { + "Channel": "Band 1", + "Frame": 2, + "Index": 2, + "IndexI": 2 + } + ], + "levels": 4, + "magnification": null, + "mm_x": null, + "mm_y": null, + "sizeX": 2048, + "sizeY": 2048, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'levels': 4,\n", + " 'sizeX': 2048,\n", + " 'sizeY': 2048,\n", + " 'tileWidth': 256,\n", + " 'tileHeight': 256,\n", + " 'magnification': None,\n", + " 'mm_x': None,\n", + " 'mm_y': None,\n", + " 'dtype': 'uint16',\n", + " 'bandCount': 3,\n", + " 'frames': [{'Channel': 'Band 1', 'Frame': 0, 'Index': 0, 'IndexI': 0},\n", + " {'Channel': 'Band 1', 'Frame': 1, 'Index': 1, 'IndexI': 1},\n", + " {'Channel': 'Band 1', 'Frame': 2, 'Index': 2, 'IndexI': 2}],\n", + " 'IndexRange': {'IndexI': 3},\n", + " 'IndexStride': {'IndexI': 1},\n", + " 'channels': ['Band 1'],\n", + " 'channelmap': {'Band 1': 0}}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# open written file as a new source\n", + "# this will be opened as a TiffFileTileSource\n", + "source_2 = large_image.open(processed_image_path)\n", + "\n", + "# view metadata\n", + "source_2.getMetadata()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "dbb8c86e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "ImageBytes<56254> (image/jpeg)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# show source_2 as a static thumbnail\n", + "source_2.getThumbnail()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d71bd65a", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "30812bb388a0426da9806e62bf5e8711", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(IntSlider(value=0, description='Frame:', max=2), Map(center=[1024.0, 1024.0], controls=(ZoomCon…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# show source_2 in an interactive viewer\n", + "source_2" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/.doctrees/nbsphinx/notebooks_large_image_examples_18_0.jpg b/.doctrees/nbsphinx/notebooks_large_image_examples_18_0.jpg new file mode 100644 index 000000000..8c93bd4a7 Binary files /dev/null and b/.doctrees/nbsphinx/notebooks_large_image_examples_18_0.jpg differ diff --git a/.doctrees/nbsphinx/notebooks_large_image_examples_6_0.jpg b/.doctrees/nbsphinx/notebooks_large_image_examples_6_0.jpg new file mode 100644 index 000000000..83c172fe9 Binary files /dev/null and b/.doctrees/nbsphinx/notebooks_large_image_examples_6_0.jpg differ diff --git a/.doctrees/nbsphinx/notebooks_zarr_sink_example_17_0.jpg b/.doctrees/nbsphinx/notebooks_zarr_sink_example_17_0.jpg new file mode 100644 index 000000000..2a510396f Binary files /dev/null and b/.doctrees/nbsphinx/notebooks_zarr_sink_example_17_0.jpg differ diff --git a/.doctrees/nbsphinx/notebooks_zarr_sink_example_7_0.jpg b/.doctrees/nbsphinx/notebooks_zarr_sink_example_7_0.jpg new file mode 100644 index 000000000..ff02b2d7d Binary files /dev/null and b/.doctrees/nbsphinx/notebooks_zarr_sink_example_7_0.jpg differ diff --git a/.doctrees/notebooks.doctree b/.doctrees/notebooks.doctree new file mode 100644 index 000000000..314927a77 Binary files /dev/null and b/.doctrees/notebooks.doctree differ diff --git a/.doctrees/notebooks/large_image_examples.doctree b/.doctrees/notebooks/large_image_examples.doctree new file mode 100644 index 000000000..fc1f57845 Binary files /dev/null and b/.doctrees/notebooks/large_image_examples.doctree differ diff --git a/.doctrees/notebooks/zarr_sink_example.doctree b/.doctrees/notebooks/zarr_sink_example.doctree new file mode 100644 index 000000000..e6f4bc6e7 Binary files /dev/null and b/.doctrees/notebooks/zarr_sink_example.doctree differ diff --git a/.doctrees/plottable.doctree b/.doctrees/plottable.doctree new file mode 100644 index 000000000..191df6336 Binary files /dev/null and b/.doctrees/plottable.doctree differ diff --git a/.doctrees/tilesource_options.doctree b/.doctrees/tilesource_options.doctree new file mode 100644 index 000000000..a6457bd1c Binary files /dev/null and b/.doctrees/tilesource_options.doctree differ diff --git a/.doctrees/upgrade.doctree b/.doctrees/upgrade.doctree new file mode 100644 index 000000000..a9f674272 Binary files /dev/null and b/.doctrees/upgrade.doctree differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/_build/girder_large_image/girder_large_image.html b/_build/girder_large_image/girder_large_image.html new file mode 100644 index 000000000..987f9f7f4 --- /dev/null +++ b/_build/girder_large_image/girder_large_image.html @@ -0,0 +1,669 @@ + + + + + + + girder_large_image package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

girder_large_image package🔗

+
+

Subpackages🔗

+
+ +
+
+
+

Submodules🔗

+
+
+

girder_large_image.constants module🔗

+
+
+class girder_large_image.constants.PluginSettings[source]🔗
+

Bases: object

+
+
+LARGE_IMAGE_AUTO_SET = 'large_image.auto_set'🔗
+
+ +
+
+LARGE_IMAGE_AUTO_USE_ALL_FILES = 'large_image.auto_use_all_files'🔗
+
+ +
+
+LARGE_IMAGE_CONFIG_FOLDER = 'large_image.config_folder'🔗
+
+ +
+
+LARGE_IMAGE_DEFAULT_VIEWER = 'large_image.default_viewer'🔗
+
+ +
+
+LARGE_IMAGE_ICC_CORRECTION = 'large_image.icc_correction'🔗
+
+ +
+
+LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE = 'large_image.max_small_image_size'🔗
+
+ +
+
+LARGE_IMAGE_MAX_THUMBNAIL_FILES = 'large_image.max_thumbnail_files'🔗
+
+ +
+
+LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK = 'large_image.notification_stream_fallback'🔗
+
+ +
+
+LARGE_IMAGE_SHOW_EXTRA = 'large_image.show_extra'🔗
+
+ +
+
+LARGE_IMAGE_SHOW_EXTRA_ADMIN = 'large_image.show_extra_admin'🔗
+
+ +
+
+LARGE_IMAGE_SHOW_EXTRA_PUBLIC = 'large_image.show_extra_public'🔗
+
+ +
+
+LARGE_IMAGE_SHOW_ITEM_EXTRA = 'large_image.show_item_extra'🔗
+
+ +
+
+LARGE_IMAGE_SHOW_ITEM_EXTRA_ADMIN = 'large_image.show_item_extra_admin'🔗
+
+ +
+
+LARGE_IMAGE_SHOW_ITEM_EXTRA_PUBLIC = 'large_image.show_item_extra_public'🔗
+
+ +
+
+LARGE_IMAGE_SHOW_THUMBNAILS = 'large_image.show_thumbnails'🔗
+
+ +
+
+LARGE_IMAGE_SHOW_VIEWER = 'large_image.show_viewer'🔗
+
+ +
+ +
+
+

girder_large_image.girder_tilesource module🔗

+
+
+class girder_large_image.girder_tilesource.GirderTileSource(item, *args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

item – a Girder item document which contains +[‘largeImage’][‘fileId’] identifying the Girder file to be used +for the tile source.

+
+
+
+
+extensionsWithAdjacentFiles = {}🔗
+
+ +
+
+static getLRUHash(*args, **kwargs)[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getState()[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+girderSource = True🔗
+
+ +
+
+mayHaveAdjacentFiles(largeImageFile)[source]🔗
+
+ +
+
+mimeTypesWithAdjacentFiles = {}🔗
+
+ +
+ +
+
+girder_large_image.girder_tilesource.getGirderTileSource(item, file=None, *args, **kwargs)[source]🔗
+

Get a Girder tilesource using the known sources.

+
+
Parameters:
+
    +
  • item – a Girder item or an item id.

  • +
  • file – if specified, the Girder file object to use as the large image +file; used here only to check extensions.

  • +
+
+
Returns:
+

A girder tilesource for the item.

+
+
+
+ +
+
+girder_large_image.girder_tilesource.getGirderTileSourceName(item, file=None, *args, **kwargs)[source]🔗
+

Get a Girder tilesource name using the known sources. If tile sources have +not yet been loaded, load them.

+
+
Parameters:
+
    +
  • item – a Girder item.

  • +
  • file – if specified, the Girder file object to use as the large image +file; used here only to check extensions.

  • +
+
+
Returns:
+

The name of a tilesource that can read the Girder item.

+
+
+
+ +
+
+girder_large_image.girder_tilesource.loadGirderTileSources()[source]🔗
+

Load all Girder tilesources from entrypoints and add them to the +AvailableGiderTileSources dictionary.

+
+ +
+
+

girder_large_image.loadmodelcache module🔗

+
+
+girder_large_image.loadmodelcache.invalidateLoadModelCache(*args, **kwargs)[source]🔗
+

Empty the LoadModelCache.

+
+ +
+
+girder_large_image.loadmodelcache.loadModel(resource, model, plugin='_core', id=None, allowCookie=False, level=None)[source]🔗
+

Load a model based on id using the current cherrypy token parameter for +authentication, caching the results. This must be called in a cherrypy +context.

+
+
Parameters:
+
    +
  • resource – the resource class instance calling the function. Used +for access to the current user and model importer.

  • +
  • model – the model name, e.g., ‘item’.

  • +
  • plugin – the plugin name when loading a plugin model.

  • +
  • id – a string id of the model to load.

  • +
  • allowCookie – true if the cookie authentication method is allowed.

  • +
  • level – access level desired.

  • +
+
+
Returns:
+

the loaded model.

+
+
+
+ +
+
+

Module contents🔗

+
+
+class girder_large_image.LargeImagePlugin(entrypoint)[source]🔗
+

Bases: GirderPlugin

+
+
+CLIENT_SOURCE_PATH = 'web_client'🔗
+

The path of the plugin’s web client source code. This path is given relative to the python +package. This property is used to link the web client source into the staging area while +building in development mode. When this value is None it indicates there is no web client +component.

+
+ +
+
+DISPLAY_NAME = 'Large Image'🔗
+

This is the named displayed to users on the plugin page. Unlike the entrypoint name +used internally, this name can be an arbitrary string.

+
+ +
+
+load(info)[source]🔗
+
+ +
+ +
+
+girder_large_image.adjustConfigForUser(config, user)[source]🔗
+

Given the current user, adjust the config so that only relevant and +combined values are used. If the root of the config dictionary contains +“access”: {“user”: <dict>, “admin”: <dict>}, the base values are updated +based on the user’s access level. If the root of the config contains +“group”: {<group-name>: <dict>, …}, the base values are updated for +every group the user is a part of.

+

The order of update is groups in C-sort alphabetical order followed by +access/user and then access/admin as they apply.

+
+
Parameters:
+

config – a config dictionary.

+
+
+
+ +
+
+girder_large_image.checkForLargeImageFiles(event)[source]🔗
+
+ +
+
+girder_large_image.handleCopyItem(event)[source]🔗
+

When copying an item, finish adjusting the largeImage fileId reference to +the copied file.

+
+ +
+
+girder_large_image.handleFileSave(event)[source]🔗
+

When a file is first saved, mark its mime type based on its extension if we +would otherwise just mark it as generic application/octet-stream.

+
+ +
+
+girder_large_image.handleRemoveFile(event)[source]🔗
+

When a file is removed, check if it is a largeImage fileId. If so, delete +the largeImage record.

+
+ +
+
+girder_large_image.handleSettingSave(event)[source]🔗
+

When certain settings are changed, clear the caches.

+
+ +
+
+girder_large_image.metadataSearchHandler(query, types, user=None, level=None, limit=0, offset=0, models=None, searchModels=None, metakey='meta')[source]🔗
+

Provide a substring search on metadata.

+
+ +
+
+girder_large_image.patchMount()[source]🔗
+
+ +
+
+girder_large_image.prepareCopyItem(event)[source]🔗
+

When copying an item, adjust the largeImage fileId reference so it can be +matched to the to-be-copied file.

+
+ +
+
+girder_large_image.removeThumbnails(event)[source]🔗
+
+ +
+
+girder_large_image.unbindGirderEventsByHandlerName(handlerName)[source]🔗
+
+ +
+
+girder_large_image.validateBoolean(doc)[source]🔗
+
+ +
+
+girder_large_image.validateBooleanOrAll(doc)[source]🔗
+
+ +
+
+girder_large_image.validateBooleanOrICCIntent(doc)[source]🔗
+
+ +
+
+girder_large_image.validateDefaultViewer(doc)[source]🔗
+
+ +
+
+girder_large_image.validateDictOrJSON(doc)[source]🔗
+
+ +
+
+girder_large_image.validateFolder(doc)[source]🔗
+
+ +
+
+girder_large_image.validateNonnegativeInteger(doc)[source]🔗
+
+ +
+
+girder_large_image.yamlConfigFile(folder, name, user)[source]🔗
+

Get a resolved named config file based on a folder and user.

+
+
Parameters:
+
    +
  • folder – a Girder folder model.

  • +
  • name – the name of the config file.

  • +
  • user – the user that the response if adjusted for.

  • +
+
+
Returns:
+

either None if no config file, or a yaml record.

+
+
+
+ +
+
+girder_large_image.yamlConfigFileWrite(folder, name, user, yaml_config)[source]🔗
+

If the user has appropriate permissions, create or modify an item in the +specified folder with the specified name, storing the config value as a +file.

+
+
Parameters:
+
    +
  • folder – a Girder folder model.

  • +
  • name – the name of the config file.

  • +
  • user – the user that the response if adjusted for.

  • +
  • yaml_config – a yaml config string.

  • +
+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/girder_large_image/girder_large_image.models.html b/_build/girder_large_image/girder_large_image.models.html new file mode 100644 index 000000000..130ecea0d --- /dev/null +++ b/_build/girder_large_image/girder_large_image.models.html @@ -0,0 +1,411 @@ + + + + + + + girder_large_image.models package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

girder_large_image.models package🔗

+
+

Submodules🔗

+
+
+

girder_large_image.models.image_item module🔗

+
+
+class girder_large_image.models.image_item.ImageItem(*args, **kwargs)[source]🔗
+

Bases: Item

+
+
+convertImage(item, fileObj, user=None, token=None, localJob=True, **kwargs)[source]🔗
+
+ +
+
+createImageItem(item, fileObj, user=None, token=None, createJob=True, notify=False, localJob=None, **kwargs)[source]🔗
+
+ +
+
+delete(item, skipFileIds=None)[source]🔗
+
+ +
+
+getAndCacheImageOrDataRun(checkAndCreate, imageFunc, item, key, keydict, pickleCache, lockkey, **kwargs)[source]🔗
+

Actually execute a cached function.

+
+ +
+
+getAssociatedImage(item, imageKey, checkAndCreate=False, *args, **kwargs)[source]🔗
+

Return an associated image.

+
+
Parameters:
+
    +
  • item – the item with the tile source.

  • +
  • imageKey – the key of the associated image to retrieve.

  • +
  • kwargs – optional arguments. Some options are width, height, +encoding, jpegQuality, jpegSubsampling, and tiffCompression.

  • +
+
+
Returns:
+

imageData, imageMime: the image data and the mime type, or +None if the associated image doesn’t exist.

+
+
+
+ +
+
+getAssociatedImagesList(item, **kwargs)[source]🔗
+

Return a list of associated images.

+
+
Parameters:
+

item – the item with the tile source.

+
+
Returns:
+

a list of keys of associated images.

+
+
+
+ +
+
+getBandInformation(item, statistics=True, **kwargs)[source]🔗
+

Using a tile source, get band information of the image.

+
+
Parameters:
+
    +
  • item – the item with the tile source.

  • +
  • kwargs – optional arguments. See the tilesource +getBandInformation method.

  • +
+
+
Returns:
+

band information.

+
+
+
+ +
+
+getInternalMetadata(item, **kwargs)[source]🔗
+
+ +
+
+getMetadata(item, **kwargs)[source]🔗
+
+ +
+
+getPixel(item, **kwargs)[source]🔗
+

Using a tile source, get a single pixel from the image.

+
+
Parameters:
+
    +
  • item – the item with the tile source.

  • +
  • kwargs – optional arguments. Some options are left, top.

  • +
+
+
Returns:
+

a dictionary of the color channel values, possibly with +additional information

+
+
+
+ +
+
+getRegion(item, **kwargs)[source]🔗
+

Using a tile source, get an arbitrary region of the image, optionally +scaling the results. Aspect ratio is preserved.

+
+
Parameters:
+
    +
  • item – the item with the tile source.

  • +
  • kwargs – optional arguments. Some options are left, top, +right, bottom, regionWidth, regionHeight, units, width, height, +encoding, jpegQuality, jpegSubsampling, and tiffCompression. This +is also passed to the tile source.

  • +
+
+
Returns:
+

regionData, regionMime: the image data and the mime type.

+
+
+
+ +
+
+getThumbnail(item, checkAndCreate=False, width=None, height=None, **kwargs)[source]🔗
+

Using a tile source, get a basic thumbnail. Aspect ratio is +preserved. If neither width nor height is given, a default value is +used. If both are given, the thumbnail will be no larger than either +size.

+
+
Parameters:
+
    +
  • item – the item with the tile source.

  • +
  • checkAndCreate – if the thumbnail is already cached, just return +True. If it does not, create, cache, and return it. If ‘nosave’, +return values from the cache, but do not store new results in the +cache.

  • +
  • width – maximum width in pixels.

  • +
  • height – maximum height in pixels.

  • +
  • kwargs – optional arguments. Some options are encoding, +jpegQuality, jpegSubsampling, tiffCompression, fill. This is also +passed to the tile source.

  • +
+
+
Returns:
+

thumbData, thumbMime: the image data and the mime type OR +a generator which will yield a file.

+
+
+
+ +
+
+getTile(item, x, y, z, mayRedirect=False, **kwargs)[source]🔗
+
+ +
+
+histogram(item, checkAndCreate=False, **kwargs)[source]🔗
+

Using a tile source, get a histogram of the image.

+
+
Parameters:
+
    +
  • item – the item with the tile source.

  • +
  • kwargs – optional arguments. See the tilesource histogram +method.

  • +
+
+
Returns:
+

histogram object.

+
+
+
+ +
+
+initialize()[source]🔗
+

Subclasses should override this and set the name of the collection as +self.name. Also, they should set any indexed fields that they require.

+
+ +
+
+removeThumbnailFiles(item, keep=0, sort=None, imageKey=None, onlyList=False, **kwargs)[source]🔗
+

Remove all large image thumbnails from an item.

+
+
Parameters:
+
    +
  • item – the item that owns the thumbnails.

  • +
  • keep – keep this many entries.

  • +
  • sort – the sort method used. The first (keep) records in this +sort order are kept.

  • +
  • imageKey – None for the basic thumbnail, otherwise an associated +imageKey.

  • +
  • onlyList – if True, return a list of known thumbnails or data +files that would be removed, but don’t remove them.

  • +
  • kwargs – additional parameters to determine which files to +remove.

  • +
+
+
Returns:
+

a tuple of (the number of files before removal, the number of +files removed).

+
+
+
+ +
+
+tileFrames(item, checkAndCreate='nosave', **kwargs)[source]🔗
+

Given the parameters for getRegion, plus a list of frames and the +number of frames across, make a larger image composed of a region from +each listed frame composited together.

+
+
Parameters:
+
    +
  • item – the item with the tile source.

  • +
  • checkAndCreate – if False, use the cache. If True and the result +is already cached, just return True. If is not, create, cache, and +return it. If ‘nosave’, return values from the cache, but do not +store new results in the cache.

  • +
  • kwargs – optional arguments. Some options are left, top, +right, bottom, regionWidth, regionHeight, units, width, height, +encoding, jpegQuality, jpegSubsampling, and tiffCompression. This +is also passed to the tile source. These also include frameList +and framesAcross.

  • +
+
+
Returns:
+

regionData, regionMime: the image data and the mime type.

+
+
+
+ +
+
+tileSource(item, **kwargs)[source]🔗
+

Get a tile source for an item.

+
+
Parameters:
+

item – the item with the tile source.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+ +
+
+

Module contents🔗

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/girder_large_image/girder_large_image.rest.html b/_build/girder_large_image/girder_large_image.rest.html new file mode 100644 index 000000000..2b8d1dbac --- /dev/null +++ b/_build/girder_large_image/girder_large_image.rest.html @@ -0,0 +1,491 @@ + + + + + + + girder_large_image.rest package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

girder_large_image.rest package🔗

+
+

Submodules🔗

+
+
+

girder_large_image.rest.item_meta module🔗

+
+
+class girder_large_image.rest.item_meta.InternalMetadataItemResource(apiRoot)[source]🔗
+

Bases: Item

+
+
+deleteMetadataKey(item, key, params)[source]🔗
+
+ +
+
+getMetadataKey(item, key, params)[source]🔗
+
+ +
+
+updateMetadataKey(item, key, params)[source]🔗
+
+ +
+ +
+
+

girder_large_image.rest.large_image_resource module🔗

+
+
+class girder_large_image.rest.large_image_resource.LargeImageResource[source]🔗
+

Bases: Resource

+
+
+cacheClear(params)[source]🔗
+
+ +
+
+cacheInfo(params)[source]🔗
+
+ +
+
+configFormat(config)[source]🔗
+
+ +
+
+configReplace(config, restart)[source]🔗
+
+ +
+
+configValidate(config)[source]🔗
+
+ +
+
+countAssociatedImages(params)[source]🔗
+
+ +
+
+countHistograms(params)[source]🔗
+
+ +
+
+countThumbnails(params)[source]🔗
+
+ +
+
+createLargeImages(folder, createJobs, localJobs, recurse, cancelJobs, redoExisting)[source]🔗
+
+ +
+
+createThumbnails(params)[source]🔗
+
+ +
+
+deleteAssociatedImages(params)[source]🔗
+
+ +
+
+deleteHistograms(params)[source]🔗
+
+ +
+
+deleteIncompleteTiles(params)[source]🔗
+
+ +
+
+deleteThumbnails(params)[source]🔗
+
+ +
+
+getPublicSettings(params)[source]🔗
+
+ +
+
+listSources(params)[source]🔗
+
+ +
+ +
+
+girder_large_image.rest.large_image_resource.createThumbnailsJob(job)[source]🔗
+
+ +
+
+girder_large_image.rest.large_image_resource.createThumbnailsJobLog(job, info, prefix='', status=None)[source]🔗
+

Log information about the create thumbnails job.

+
+
Parameters:
+
    +
  • job – the job object.

  • +
  • info – a dictionary with the number of thumbnails checked, created, +and failed.

  • +
  • prefix – a string to place in front of the log message.

  • +
  • status – if not None, a new status for the job.

  • +
+
+
+
+ +
+
+girder_large_image.rest.large_image_resource.createThumbnailsJobTask(item, spec)[source]🔗
+

For an individual item, check or create all of the appropriate thumbnails.

+
+
Parameters:
+
    +
  • item – the image item.

  • +
  • spec – a list of thumbnail specifications.

  • +
+
+
Returns:
+

a dictionary with the total status of the thumbnail job.

+
+
+
+ +
+
+girder_large_image.rest.large_image_resource.createThumbnailsJobThread(job)[source]🔗
+

Create thumbnails for all of the large image items.

+

The job object contains:

+
- spec: an array, each entry of which is the parameter dictionary
+  for the model getThumbnail function.
+- logInterval: the time in seconds between log messages.  This
+  also controls the granularity of cancelling the job.
+- concurrent: the number of threads to use.  0 for the number of
+  cpus.
+
+
+
+
Parameters:
+

job – the job object including kwargs.

+
+
+
+ +
+
+girder_large_image.rest.large_image_resource.cursorNextOrNone(cursor)[source]🔗
+

Given a Mongo cursor, return the next value if there is one. If not, +return None.

+
+
Parameters:
+

cursor – a cursor to get a value from.

+
+
Returns:
+

the next value or None.

+
+
+
+ +
+
+

girder_large_image.rest.tiles module🔗

+
+
+class girder_large_image.rest.tiles.TilesItemResource(apiRoot)[source]🔗
+

Bases: Item

+
+
+addTilesThumbnails(item, key, mimeType, thumbnail=False, data=None)[source]🔗
+
+ +
+
+convertImage(item, params)[source]🔗
+
+ +
+
+createTiles(item, params)[source]🔗
+
+ +
+
+deleteTiles(item, params)[source]🔗
+
+ +
+
+deleteTilesThumbnails(item, keep, key=None, thumbnail=True)[source]🔗
+
+ +
+
+getAssociatedImage(itemId, image, params)[source]🔗
+
+ +
+
+getAssociatedImageMetadata(item, image, params)[source]🔗
+
+ +
+
+getAssociatedImagesList(item, params)[source]🔗
+
+ +
+
+getBandInformation(item, params)[source]🔗
+
+ +
+
+getDZIInfo(item, params)[source]🔗
+
+ +
+
+getDZITile(item, level, xandy, params)[source]🔗
+
+ +
+
+getHistogram(item, params)[source]🔗
+
+ +
+
+getInternalMetadata(item, params)[source]🔗
+
+ +
+
+getTestTile(z, x, y, params)[source]🔗
+
+ +
+
+getTestTilesInfo(params)[source]🔗
+
+ +
+
+getTile(itemId, z, x, y, params)[source]🔗
+
+ +
+
+getTileWithFrame(itemId, frame, z, x, y, params)[source]🔗
+
+ +
+
+getTilesInfo(item, params)[source]🔗
+
+ +
+
+getTilesPixel(item, params)[source]🔗
+
+ +
+
+getTilesRegion(item, params)[source]🔗
+
+ +
+
+getTilesThumbnail(item, params)[source]🔗
+
+ +
+
+listTilesThumbnails(item)[source]🔗
+
+ +
+
+tileFrames(item, params)[source]🔗
+
+ +
+
+tileFramesQuadInfo(item, params)[source]🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+girder_large_image.rest.addSystemEndpoints(apiRoot)[source]🔗
+

This adds endpoints to routes that already exist in Girder.

+
+
Parameters:
+

apiRoot – Girder api root class.

+
+
+
+ +
+
+girder_large_image.rest.getYAMLConfigFile(self, folder, name)[source]🔗
+
+ +
+
+girder_large_image.rest.putYAMLConfigFile(self, folder, name, config)[source]🔗
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/girder_large_image/modules.html b/_build/girder_large_image/modules.html new file mode 100644 index 000000000..c6863c511 --- /dev/null +++ b/_build/girder_large_image/modules.html @@ -0,0 +1,234 @@ + + + + + + + girder_large_image — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

girder_large_image🔗

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/girder_large_image_annotation/girder_large_image_annotation.html b/_build/girder_large_image_annotation/girder_large_image_annotation.html new file mode 100644 index 000000000..f80746080 --- /dev/null +++ b/_build/girder_large_image_annotation/girder_large_image_annotation.html @@ -0,0 +1,401 @@ + + + + + + + girder_large_image_annotation package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

girder_large_image_annotation package🔗

+
+

Subpackages🔗

+
+ +
+
+
+

Submodules🔗

+
+
+

girder_large_image_annotation.constants module🔗

+
+
+

girder_large_image_annotation.handlers module🔗

+
+
+girder_large_image_annotation.handlers.process_annotations(event)[source]🔗
+

Add annotations to an image on a data.process event

+
+ +
+
+girder_large_image_annotation.handlers.resolveAnnotationGirderIds(event, results, data, possibleGirderIds)[source]🔗
+

If an annotation has references to girderIds, resolve them to actual ids.

+
+
Parameters:
+
    +
  • event – a data.process event.

  • +
  • results – the results from _itemFromEvent,

  • +
  • data – annotation data.

  • +
  • possibleGirderIds – a list of annotation elements with girderIds +needing resolution.

  • +
+
+
Returns:
+

True if all ids were processed.

+
+
+
+ +
+
+

Module contents🔗

+
+
+class girder_large_image_annotation.LargeImageAnnotationPlugin(entrypoint)[source]🔗
+

Bases: GirderPlugin

+
+
+CLIENT_SOURCE_PATH = 'web_client'🔗
+

The path of the plugin’s web client source code. This path is given relative to the python +package. This property is used to link the web client source into the staging area while +building in development mode. When this value is None it indicates there is no web client +component.

+
+ +
+
+DISPLAY_NAME = 'Large Image Annotation'🔗
+

This is the named displayed to users on the plugin page. Unlike the entrypoint name +used internally, this name can be an arbitrary string.

+
+ +
+
+load(info)[source]🔗
+
+ +
+ +
+
+girder_large_image_annotation.metadataSearchHandler(*args, **kwargs)[source]🔗
+
+ +
+
+girder_large_image_annotation.validateBoolean(doc)[source]🔗
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/girder_large_image_annotation/girder_large_image_annotation.models.html b/_build/girder_large_image_annotation/girder_large_image_annotation.models.html new file mode 100644 index 000000000..d666aa6a3 --- /dev/null +++ b/_build/girder_large_image_annotation/girder_large_image_annotation.models.html @@ -0,0 +1,880 @@ + + + + + + + girder_large_image_annotation.models package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

girder_large_image_annotation.models package🔗

+
+

Submodules🔗

+
+
+

girder_large_image_annotation.models.annotation module🔗

+
+
+class girder_large_image_annotation.models.annotation.Annotation(*args, **kwargs)[source]🔗
+

Bases: AccessControlledModel

+

This model is used to represent an annotation that is associated with an +item. The annotation can contain any number of annotationelements, which +are included because they reference this annotation as a parent. The +annotation acts like these are a native part of it, though they are each +stored as independent models to (eventually) permit faster spatial +searching.

+
+
+class Skill(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]🔗
+

Bases: Enum

+
+
+EXPERT = 'expert'🔗
+
+ +
+
+NOVICE = 'novice'🔗
+
+ +
+ +
+
+baseFields = ('_id', 'itemId', 'creatorId', 'created', 'updated', 'updatedId', 'public', 'publicFlags', 'groups')🔗
+
+ +
+
+createAnnotation(item, creator, annotation, public=None)[source]🔗
+
+ +
+
+deleteMetadata(annotation, fields)[source]🔗
+

Delete metadata on an annotation. A ValidationException is thrown if +the metadata field names contain a period (‘.’) or begin with a dollar +sign (‘$’).

+
+
Parameters:
+
    +
  • annotation (dict) – The annotation to delete metadata from.

  • +
  • fields – An array containing the field names to delete from the +annotation’s meta field

  • +
+
+
Returns:
+

the annotation document

+
+
+
+ +
+
+findAnnotatedImages(imageNameFilter=None, creator=None, user=None, level=2, force=None, offset=0, limit=0, sort=None, **kwargs)[source]🔗
+

Find images associated with annotations.

+

The list returned by this function is paginated and filtered by access control using +the standard girder kwargs.

+
+
Parameters:
+
    +
  • imageNameFilter – A string used to filter images by name. An image name matches +if it (or a subtoken) begins with this string. Subtokens are generated by splitting +by the regex [\W_]+ This filter is case-insensitive.

  • +
  • creator – Filter by a user who is the creator of the annotation.

  • +
+
+
+
+ +
+
+geojson(annotation)[source]🔗
+

Yield an annotation as geojson generator.

+
+
Parameters:
+

annotation – The annotation to delete metadata from.

+
+
Yields:
+

geojson. General annotation properties are added to the first +feature under the annotation tag.

+
+
+
+ +
+
+getVersion(annotationId, version, user=None, force=False, *args, **kwargs)[source]🔗
+

Get an annotation history version. This reconstructs the original +annotation.

+
+
Parameters:
+
    +
  • annotationId – the annotation to get history for.

  • +
  • version – the specific version to get.

  • +
  • user – the Girder user. If the user is not an admin, they must +have read access on the item and the item must exist.

  • +
  • force – if True, don’t get the user access.

  • +
+
+
+
+ +
+
+idRegex = re.compile('^[0-9a-f]{24}$')🔗
+
+ +
+
+initialize()[source]🔗
+

Subclasses should override this and set the name of the collection as +self.name. Also, they should set any indexed fields that they require.

+
+ +
+
+injectAnnotationGroupSet(annotation)[source]🔗
+
+ +
+
+load(id, region=None, getElements=True, *args, **kwargs)[source]🔗
+

Load an annotation, adding all or a subset of the elements to it.

+
+
Parameters:
+
    +
  • region – if present, a dictionary restricting which annotations +are returned. See annotationelement.getElements.

  • +
  • getElements – if False, don’t get elements associated with this +annotation.

  • +
+
+
Returns:
+

the matching annotation or none.

+
+
+
+ +
+
+numberInstance = (<class 'int'>, <class 'float'>)🔗
+
+ +
+
+remove(annotation, *args, **kwargs)[source]🔗
+

When removing an annotation, remove all element associated with it. +This overrides the collection delete_one method so that all of the +triggers are fired as expected and cancelling from an event will work +as needed.

+
+
Parameters:
+

annotation – the annotation document to remove.

+
+
+
+ +
+
+removeOldAnnotations(remove=False, minAgeInDays=30, keepInactiveVersions=5)[source]🔗
+

Remove annotations that (a) have no item or (b) are inactive and at +least (1) a minimum age in days and (2) not the most recent inactive +versions. Also remove any annotation elements that don’t have +associated annotations and are a minimum age in days.

+
+
Parameters:
+
    +
  • remove – if False, just report on what would be done. If true, +actually remove the annotations and compact the collections.

  • +
  • minAgeInDays – only work on annotations that are at least this +old. This must be greater than or equal to 7.

  • +
  • keepInactiveVersions – keep at least this many inactive versions +of any annotation, regardless of age.

  • +
+
+
+
+ +
+
+revertVersion(id, version=None, user=None, force=False)[source]🔗
+

Revert to a previous version of an annotation.

+
+
Parameters:
+
    +
  • id – the annotation id.

  • +
  • version – the version to revert to. None reverts to the previous +version. If the annotation was deleted, this is the most recent +version.

  • +
  • user – the user doing the reversion.

  • +
  • force – if True don’t authenticate the user with the associated +item access.

  • +
+
+
+
+ +
+
+save(annotation, *args, **kwargs)[source]🔗
+

When saving an annotation, override the collection insert_one and +replace_one methods so that we don’t save the elements with the main +annotation. Still use the super class’s save method, so that all of +the triggers are fired as expected and cancelling and modifications can +be done as needed.

+

Because Mongo doesn’t support transactions, a version number is stored +with the annotation and with the associated elements. This is used to +add the new elements first, then update the annotation, and delete the +old elements. The allows version integrity if another thread queries +the annotation at the same time.

+
+
Parameters:
+

annotation – the annotation document to save.

+
+
Returns:
+

the saved document. If it is a new document, the _id has +been added.

+
+
+
+ +
+
+setAccessList(doc, access, save=False, **kwargs)[source]🔗
+

The super class’s setAccessList function can save a document. However, +annotations which have not loaded elements lose their elements when +this occurs, because the validation step of the save function adds an +empty element list. By using an update instead of a save, this +prevents the problem.

+
+ +
+
+setMetadata(annotation, metadata, allowNull=False)[source]🔗
+

Set metadata on an annotation. A ValidationException is thrown in +the cases where the metadata JSON object is badly formed, or if any of +the metadata keys contains a period (‘.’).

+
+
Parameters:
+
    +
  • annotation (dict) – The annotation to set the metadata on.

  • +
  • metadata (dict) – A dictionary containing key-value pairs to add to +the annotations meta field

  • +
  • allowNull – Whether to allow null values to be set in the +annotation’s metadata. If set to False or omitted, a null value +will cause that metadata field to be deleted.

  • +
+
+
Returns:
+

the annotation document

+
+
+
+ +
+
+updateAnnotation(annotation, updateUser=None)[source]🔗
+

Update an annotation.

+
+
Parameters:
+
    +
  • annotation – the annotation document to update.

  • +
  • updateUser – the user who is creating the update.

  • +
+
+
Returns:
+

the annotation document that was updated.

+
+
+
+ +
+
+validate(doc)[source]🔗
+

Models should implement this to validate the document before it enters +the database. It must return the document with any necessary filters +applied, or throw a ValidationException if validation of the document +fails.

+
+
Parameters:
+

doc (dict) – The document to validate before saving to the collection.

+
+
+
+ +
+
+validatorAnnotation = Draft6Validator(schema={'$schema': 'http://json-...a.org/schema#', 'additionalProperties': False, 'properties': {'attributes': {'additionalProperties': True, 'description': 'Subjective t...entire image.', 'title': 'Image Attributes', 'type': 'object'}, 'description': {'type': 'string'}, 'display': {'properties': {'visible': {'description': 'This advises...is displayed.', 'enum': ['new', True, False], 'type': ['boolean', 'string']}}, 'type': 'object'}, 'elements': {'description': 'Subjective t...atial region.', 'items': {'anyOf': [{'additionalProperties': False, 'description': 'The first po... of the arrow', 'properties': {...}, 'required': [...], ...}, {'additionalProperties': False, 'properties': {...}, 'required': [...], 'type': 'object'}, {'additionalProperties': False, 'decription': 'normal is th...ise specified', 'properties': {...}, 'required': [...], ...}, {'additionalProperties': False, 'description': 'ColorRange a... rangeValues.', 'properties': {...}, 'required': [...], ...}, {'additionalProperties': False, 'description': 'ColorRange a...rrespondence.', 'properties': {...}, 'required': [...], ...}, {'additionalProperties': False, 'properties': {...}, 'required': [...], 'type': 'object'}, ...]}, 'title': 'Image Markup', 'type': 'array'}, ...}, 'type': 'object'}, format_checker=None)🔗
+
+ +
+
+validatorAnnotationElement = Draft6Validator(schema={'anyOf': [{'additionalProperties': False, 'description': 'The first po... of the arrow', 'properties': {'fillColor': {'pattern': '^(#([0-9a-fA...\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {...}, 'fontSize': {...}, 'value': {...}, 'visibility': {...}}, 'required': ['value'], 'type': 'object'}, ...}, 'required': ['points', 'type'], ...}, {'additionalProperties': False, 'properties': {'center': {'description': 'An X, Y, Z c...e upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, ...}, 'fillColor': {'pattern': '^(#([0-9a-fA...\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, ...}, 'required': ['center', 'radius', 'type'], 'type': 'object'}, {'additionalProperties': False, 'decription': 'normal is th...ise specified', 'properties': {'center': {'description': 'An X, Y, Z c...e upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, ...}, 'fillColor': {'pattern': '^(#([0-9a-fA...\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, ...}, 'required': ['center', 'height', 'type', 'width'], ...}, {'additionalProperties': False, 'description': 'ColorRange a... rangeValues.', 'properties': {'colorRange': {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA...\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}, 'dx': {'description': 'grid spacing...e x direction', 'type': 'number'}, 'dy': {'description': 'grid spacing...e y direction', 'type': 'number'}, 'gridWidth': {'description': 'The number o...h of the grid', 'minimum': 1, 'type': 'integer'}, ...}, 'required': ['gridWidth', 'type', 'values'], ...}, {'additionalProperties': False, 'description': 'ColorRange a...rrespondence.', 'properties': {'colorRange': {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA...\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {...}, 'fontSize': {...}, 'value': {...}, 'visibility': {...}}, 'required': ['value'], 'type': 'object'}, ...}, 'required': ['points', 'type'], ...}, {'additionalProperties': False, 'properties': {'center': {'description': 'An X, Y, Z c...e upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, ...}, 'fillColor': {'pattern': '^(#([0-9a-fA...\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, ...}, 'required': ['center', 'type'], 'type': 'object'}, ...]}, format_checker=None)🔗
+
+ +
+
+versionList(annotationId, user=None, limit=0, offset=0, sort=(('_version', -1),), force=False)[source]🔗
+

List annotation history entries for a specific annotationId. Only +annotations that belong to an existing item that the user is allowed to +view are included. If the user is an admin, all annotations will be +included.

+
+
Parameters:
+
    +
  • annotationId – the annotation to get history for.

  • +
  • user – the Girder user.

  • +
  • limit – maximum number of history entries to return.

  • +
  • offset – skip this many entries.

  • +
  • sort – the sort method used. Defaults to reverse _id.

  • +
  • force – if True, don’t authenticate the user.

  • +
+
+
Yields:
+

the entries in the list

+
+
+
+ +
+ +
+
+class girder_large_image_annotation.models.annotation.AnnotationSchema[source]🔗
+

Bases: object

+
+
+annotationElementSchema = {'anyOf': [{'additionalProperties': False, 'description': 'The first point is the head of the arrow', 'properties': {'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'points': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'type': {'enum': ['arrow'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}, {'additionalProperties': False, 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'radius': {'minimum': 0, 'type': 'number'}, 'type': {'enum': ['circle'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['center', 'radius', 'type'], 'type': 'object'}, {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['ellipse'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}}, 'required': ['center', 'height', 'type', 'width'], 'type': 'object'}, {'additionalProperties': False, 'description': 'ColorRange and rangeValues should have a one-to-one correspondence except for stepped contours where rangeValues needs one more entry than colorRange.  minColor and maxColor are the colors applies to values beyond the ranges in rangeValues.', 'properties': {'colorRange': {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}, 'dx': {'description': 'grid spacing in the x direction', 'type': 'number'}, 'dy': {'description': 'grid spacing in the y direction', 'type': 'number'}, 'gridWidth': {'description': 'The number of values across the width of the grid', 'minimum': 1, 'type': 'integer'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'interpretation': {'enum': ['heatmap', 'contour', 'choropleth'], 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'maxColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'minColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'normalizeRange': {'description': 'If true, rangeValues are on a scale of 0 to 1 and map to the minimum and maximum values on the data.  If false (the default), the rangeValues are the actual data values.', 'type': 'boolean'}, 'origin': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'radius': {'description': 'radius used for heatmap interpretation', 'exclusiveMinimum': 0, 'type': 'number'}, 'rangeValues': {'description': 'A weakly monotonic list of range values', 'items': {'type': 'number'}, 'type': 'array'}, 'stepped': {'type': 'boolean'}, 'type': {'enum': ['griddata'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'values': {'description': 'The values of the grid.  This must have a multiple of gridWidth entries', 'items': {'type': 'number'}, 'type': 'array'}}, 'required': ['gridWidth', 'type', 'values'], 'type': 'object'}, {'additionalProperties': False, 'description': 'ColorRange and rangeValues should have a one-to-one correspondence.', 'properties': {'colorRange': {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'normalizeRange': {'description': 'If true, rangeValues are on a scale of 0 to 1 and map to the minimum and maximum values on the data.  If false (the default), the rangeValues are the actual data values.', 'type': 'boolean'}, 'points': {'items': {'description': 'An X, Y, Z, value coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 4, 'minItems': 4, 'name': 'CoordinateWithValue', 'type': 'array'}, 'type': 'array'}, 'radius': {'exclusiveMinimum': 0, 'type': 'number'}, 'rangeValues': {'description': 'A weakly monotonic list of range values', 'items': {'type': 'number'}, 'type': 'array'}, 'scaleWithZoom': {'description': 'If true, scale the size of points with the zoom level of the map.', 'type': 'boolean'}, 'type': {'enum': ['heatmap'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}, {'additionalProperties': False, 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'type': {'enum': ['point'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['center', 'type'], 'type': 'object'}, {'additionalProperties': False, 'properties': {'closed': {'description': 'polyline is open if closed flag is not specified', 'type': 'boolean'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'holes': {'description': 'If closed is true, this is a list of polylines that are treated as holes in the base polygon. These should not cross each other and should be contained within the base polygon.', 'items': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'minItems': 3, 'type': 'array'}, 'type': 'array'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'points': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'minItems': 2, 'type': 'array'}, 'type': {'enum': ['polyline'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}, {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['rectangle'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}}, 'required': ['center', 'height', 'type', 'width'], 'type': 'object'}, {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'heightSubdivisions': {'minimum': 1, 'type': 'integer'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['rectanglegrid'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}, 'widthSubdivisions': {'minimum': 1, 'type': 'integer'}}, 'required': ['center', 'height', 'heightSubdivisions', 'type', 'width', 'widthSubdivisions'], 'type': 'object'}, {'additionalProperties': False, 'description': 'An image overlay on top of the base resource.', 'properties': {'girderId': {'description': 'Girder item ID containing the image to overlay.', 'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'group': {'type': 'string'}, 'hasAlpha': {'description': 'If true, the image is treated assuming it has an alpha channel.', 'type': 'boolean'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'opacity': {'description': 'Default opacity for this image overlay. Must be between 0 and 1. Defaults to 1.', 'maximum': 1, 'minimum': 0, 'type': 'number'}, 'transform': {'description': 'Specification for an affine transform of the image overlay. Includes a 2D transform matrix, an X offset and a Y offset.', 'properties': {'matrix': {'description': 'A 2D matrix representing the transform of an image overlay.', 'items': {'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'xoffset': {'type': 'number'}, 'yoffset': {'type': 'number'}}, 'type': 'object'}, 'type': {'enum': ['image'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['girderId', 'type'], 'type': 'object'}, {'additionalProperties': False, 'description': 'A tiled pixelmap to overlay onto a base resource.', 'properties': {'boundaries': {'description': 'True if the pixelmap doubles pixel values such that even values are the fill and odd values the are stroke of each superpixel. If true, the length of the values array should be half of the maximum value in the pixelmap.', 'type': 'boolean'}, 'categories': {'description': 'An array used to map between the values array and color values. Can also contain semantic information for color values.', 'items': {'additionalProperties': False, 'properties': {'description': {'description': 'A more detailed explanation of the meaining of this category.', 'type': 'string'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'label': {'description': 'A string representing the semantic meaning of regions of the map with the corresponding color.', 'type': 'string'}, 'strokeColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}}, 'required': ['fillColor'], 'type': 'object'}, 'type': 'array'}, 'girderId': {'description': 'Girder item ID containing the image to overlay.', 'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'group': {'type': 'string'}, 'hasAlpha': {'description': 'If true, the image is treated assuming it has an alpha channel.', 'type': 'boolean'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'opacity': {'description': 'Default opacity for this image overlay. Must be between 0 and 1. Defaults to 1.', 'maximum': 1, 'minimum': 0, 'type': 'number'}, 'transform': {'description': 'Specification for an affine transform of the image overlay. Includes a 2D transform matrix, an X offset and a Y offset.', 'properties': {'matrix': {'description': 'A 2D matrix representing the transform of an image overlay.', 'items': {'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'xoffset': {'type': 'number'}, 'yoffset': {'type': 'number'}}, 'type': 'object'}, 'type': {'enum': ['pixelmap'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'values': {'description': 'An array where the indices correspond to pixel values in the pixel map image and the values are used to look up the appropriate color in the categories property.', 'items': {'type': 'integer'}, 'type': 'array'}}, 'required': ['boundaries', 'categories', 'girderId', 'type', 'values'], 'type': 'object'}]}🔗
+
+ +
+
+annotationSchema = {'$schema': 'http://json-schema.org/schema#', 'additionalProperties': False, 'properties': {'attributes': {'additionalProperties': True, 'description': 'Subjective things that apply to the entire image.', 'title': 'Image Attributes', 'type': 'object'}, 'description': {'type': 'string'}, 'display': {'properties': {'visible': {'description': 'This advises viewers on when the annotation should be shown.  If "new" (the default), show the annotation when it is first added to the system.  If false, don\'t show the annotation by default.  If true, show the annotation when the item is displayed.', 'enum': ['new', True, False], 'type': ['boolean', 'string']}}, 'type': 'object'}, 'elements': {'description': 'Subjective things that apply to a spatial region.', 'items': {'anyOf': [{'additionalProperties': False, 'description': 'The first point is the head of the arrow', 'properties': {'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'points': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'type': {'enum': ['arrow'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}, {'additionalProperties': False, 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'radius': {'minimum': 0, 'type': 'number'}, 'type': {'enum': ['circle'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['center', 'radius', 'type'], 'type': 'object'}, {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['ellipse'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}}, 'required': ['center', 'height', 'type', 'width'], 'type': 'object'}, {'additionalProperties': False, 'description': 'ColorRange and rangeValues should have a one-to-one correspondence except for stepped contours where rangeValues needs one more entry than colorRange.  minColor and maxColor are the colors applies to values beyond the ranges in rangeValues.', 'properties': {'colorRange': {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}, 'dx': {'description': 'grid spacing in the x direction', 'type': 'number'}, 'dy': {'description': 'grid spacing in the y direction', 'type': 'number'}, 'gridWidth': {'description': 'The number of values across the width of the grid', 'minimum': 1, 'type': 'integer'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'interpretation': {'enum': ['heatmap', 'contour', 'choropleth'], 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'maxColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'minColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'normalizeRange': {'description': 'If true, rangeValues are on a scale of 0 to 1 and map to the minimum and maximum values on the data.  If false (the default), the rangeValues are the actual data values.', 'type': 'boolean'}, 'origin': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'radius': {'description': 'radius used for heatmap interpretation', 'exclusiveMinimum': 0, 'type': 'number'}, 'rangeValues': {'description': 'A weakly monotonic list of range values', 'items': {'type': 'number'}, 'type': 'array'}, 'stepped': {'type': 'boolean'}, 'type': {'enum': ['griddata'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'values': {'description': 'The values of the grid.  This must have a multiple of gridWidth entries', 'items': {'type': 'number'}, 'type': 'array'}}, 'required': ['gridWidth', 'type', 'values'], 'type': 'object'}, {'additionalProperties': False, 'description': 'ColorRange and rangeValues should have a one-to-one correspondence.', 'properties': {'colorRange': {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'normalizeRange': {'description': 'If true, rangeValues are on a scale of 0 to 1 and map to the minimum and maximum values on the data.  If false (the default), the rangeValues are the actual data values.', 'type': 'boolean'}, 'points': {'items': {'description': 'An X, Y, Z, value coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 4, 'minItems': 4, 'name': 'CoordinateWithValue', 'type': 'array'}, 'type': 'array'}, 'radius': {'exclusiveMinimum': 0, 'type': 'number'}, 'rangeValues': {'description': 'A weakly monotonic list of range values', 'items': {'type': 'number'}, 'type': 'array'}, 'scaleWithZoom': {'description': 'If true, scale the size of points with the zoom level of the map.', 'type': 'boolean'}, 'type': {'enum': ['heatmap'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}, {'additionalProperties': False, 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'type': {'enum': ['point'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['center', 'type'], 'type': 'object'}, {'additionalProperties': False, 'properties': {'closed': {'description': 'polyline is open if closed flag is not specified', 'type': 'boolean'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'holes': {'description': 'If closed is true, this is a list of polylines that are treated as holes in the base polygon. These should not cross each other and should be contained within the base polygon.', 'items': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'minItems': 3, 'type': 'array'}, 'type': 'array'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'points': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'minItems': 2, 'type': 'array'}, 'type': {'enum': ['polyline'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}, {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['rectangle'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}}, 'required': ['center', 'height', 'type', 'width'], 'type': 'object'}, {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'heightSubdivisions': {'minimum': 1, 'type': 'integer'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['rectanglegrid'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}, 'widthSubdivisions': {'minimum': 1, 'type': 'integer'}}, 'required': ['center', 'height', 'heightSubdivisions', 'type', 'width', 'widthSubdivisions'], 'type': 'object'}, {'additionalProperties': False, 'description': 'An image overlay on top of the base resource.', 'properties': {'girderId': {'description': 'Girder item ID containing the image to overlay.', 'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'group': {'type': 'string'}, 'hasAlpha': {'description': 'If true, the image is treated assuming it has an alpha channel.', 'type': 'boolean'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'opacity': {'description': 'Default opacity for this image overlay. Must be between 0 and 1. Defaults to 1.', 'maximum': 1, 'minimum': 0, 'type': 'number'}, 'transform': {'description': 'Specification for an affine transform of the image overlay. Includes a 2D transform matrix, an X offset and a Y offset.', 'properties': {'matrix': {'description': 'A 2D matrix representing the transform of an image overlay.', 'items': {'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'xoffset': {'type': 'number'}, 'yoffset': {'type': 'number'}}, 'type': 'object'}, 'type': {'enum': ['image'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['girderId', 'type'], 'type': 'object'}, {'additionalProperties': False, 'description': 'A tiled pixelmap to overlay onto a base resource.', 'properties': {'boundaries': {'description': 'True if the pixelmap doubles pixel values such that even values are the fill and odd values the are stroke of each superpixel. If true, the length of the values array should be half of the maximum value in the pixelmap.', 'type': 'boolean'}, 'categories': {'description': 'An array used to map between the values array and color values. Can also contain semantic information for color values.', 'items': {'additionalProperties': False, 'properties': {'description': {'description': 'A more detailed explanation of the meaining of this category.', 'type': 'string'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'label': {'description': 'A string representing the semantic meaning of regions of the map with the corresponding color.', 'type': 'string'}, 'strokeColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}}, 'required': ['fillColor'], 'type': 'object'}, 'type': 'array'}, 'girderId': {'description': 'Girder item ID containing the image to overlay.', 'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'group': {'type': 'string'}, 'hasAlpha': {'description': 'If true, the image is treated assuming it has an alpha channel.', 'type': 'boolean'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'opacity': {'description': 'Default opacity for this image overlay. Must be between 0 and 1. Defaults to 1.', 'maximum': 1, 'minimum': 0, 'type': 'number'}, 'transform': {'description': 'Specification for an affine transform of the image overlay. Includes a 2D transform matrix, an X offset and a Y offset.', 'properties': {'matrix': {'description': 'A 2D matrix representing the transform of an image overlay.', 'items': {'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'xoffset': {'type': 'number'}, 'yoffset': {'type': 'number'}}, 'type': 'object'}, 'type': {'enum': ['pixelmap'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'values': {'description': 'An array where the indices correspond to pixel values in the pixel map image and the values are used to look up the appropriate color in the categories property.', 'items': {'type': 'integer'}, 'type': 'array'}}, 'required': ['boundaries', 'categories', 'girderId', 'type', 'values'], 'type': 'object'}]}, 'title': 'Image Markup', 'type': 'array'}, 'name': {'minLength': 1, 'type': 'string'}}, 'type': 'object'}🔗
+
+ +
+
+arrowShapeSchema = {'additionalProperties': False, 'description': 'The first point is the head of the arrow', 'properties': {'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'points': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'type': {'enum': ['arrow'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}🔗
+
+ +
+
+baseElementSchema = {'additionalProperties': True, 'properties': {'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'type': {'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['type'], 'type': 'object'}🔗
+
+ +
+
+baseRectangleShapeSchema = {'additionalProperties': True, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}}, 'required': ['center', 'height', 'type', 'width'], 'type': 'object'}🔗
+
+ +
+
+baseShapeSchema = {'additionalProperties': True, 'properties': {'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'type': {'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['type'], 'type': 'object'}🔗
+
+ +
+
+circleShapeSchema = {'additionalProperties': False, 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'radius': {'minimum': 0, 'type': 'number'}, 'type': {'enum': ['circle'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['center', 'radius', 'type'], 'type': 'object'}🔗
+
+ +
+
+colorRangeSchema = {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}🔗
+
+ +
+
+colorSchema = {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}🔗
+
+ +
+
+coordSchema = {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}🔗
+
+ +
+
+coordValueSchema = {'description': 'An X, Y, Z, value coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 4, 'minItems': 4, 'name': 'CoordinateWithValue', 'type': 'array'}🔗
+
+ +
+
+ellipseShapeSchema = {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['ellipse'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}}, 'required': ['center', 'height', 'type', 'width'], 'type': 'object'}🔗
+
+ +
+
+griddataSchema = {'additionalProperties': False, 'description': 'ColorRange and rangeValues should have a one-to-one correspondence except for stepped contours where rangeValues needs one more entry than colorRange.  minColor and maxColor are the colors applies to values beyond the ranges in rangeValues.', 'properties': {'colorRange': {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}, 'dx': {'description': 'grid spacing in the x direction', 'type': 'number'}, 'dy': {'description': 'grid spacing in the y direction', 'type': 'number'}, 'gridWidth': {'description': 'The number of values across the width of the grid', 'minimum': 1, 'type': 'integer'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'interpretation': {'enum': ['heatmap', 'contour', 'choropleth'], 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'maxColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'minColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'normalizeRange': {'description': 'If true, rangeValues are on a scale of 0 to 1 and map to the minimum and maximum values on the data.  If false (the default), the rangeValues are the actual data values.', 'type': 'boolean'}, 'origin': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'radius': {'description': 'radius used for heatmap interpretation', 'exclusiveMinimum': 0, 'type': 'number'}, 'rangeValues': {'description': 'A weakly monotonic list of range values', 'items': {'type': 'number'}, 'type': 'array'}, 'stepped': {'type': 'boolean'}, 'type': {'enum': ['griddata'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'values': {'description': 'The values of the grid.  This must have a multiple of gridWidth entries', 'items': {'type': 'number'}, 'type': 'array'}}, 'required': ['gridWidth', 'type', 'values'], 'type': 'object'}🔗
+
+ +
+
+groupSchema = {'type': 'string'}🔗
+
+ +
+
+heatmapSchema = {'additionalProperties': False, 'description': 'ColorRange and rangeValues should have a one-to-one correspondence.', 'properties': {'colorRange': {'description': 'A list of colors', 'items': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'type': 'array'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'normalizeRange': {'description': 'If true, rangeValues are on a scale of 0 to 1 and map to the minimum and maximum values on the data.  If false (the default), the rangeValues are the actual data values.', 'type': 'boolean'}, 'points': {'items': {'description': 'An X, Y, Z, value coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 4, 'minItems': 4, 'name': 'CoordinateWithValue', 'type': 'array'}, 'type': 'array'}, 'radius': {'exclusiveMinimum': 0, 'type': 'number'}, 'rangeValues': {'description': 'A weakly monotonic list of range values', 'items': {'type': 'number'}, 'type': 'array'}, 'scaleWithZoom': {'description': 'If true, scale the size of points with the zoom level of the map.', 'type': 'boolean'}, 'type': {'enum': ['heatmap'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}🔗
+
+ +
+
+labelSchema = {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}🔗
+
+ +
+
+overlaySchema = {'additionalProperties': False, 'description': 'An image overlay on top of the base resource.', 'properties': {'girderId': {'description': 'Girder item ID containing the image to overlay.', 'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'group': {'type': 'string'}, 'hasAlpha': {'description': 'If true, the image is treated assuming it has an alpha channel.', 'type': 'boolean'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'opacity': {'description': 'Default opacity for this image overlay. Must be between 0 and 1. Defaults to 1.', 'maximum': 1, 'minimum': 0, 'type': 'number'}, 'transform': {'description': 'Specification for an affine transform of the image overlay. Includes a 2D transform matrix, an X offset and a Y offset.', 'properties': {'matrix': {'description': 'A 2D matrix representing the transform of an image overlay.', 'items': {'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'xoffset': {'type': 'number'}, 'yoffset': {'type': 'number'}}, 'type': 'object'}, 'type': {'enum': ['image'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['girderId', 'type'], 'type': 'object'}🔗
+
+ +
+
+pixelmapCategorySchema = {'additionalProperties': False, 'properties': {'description': {'description': 'A more detailed explanation of the meaining of this category.', 'type': 'string'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'label': {'description': 'A string representing the semantic meaning of regions of the map with the corresponding color.', 'type': 'string'}, 'strokeColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}}, 'required': ['fillColor'], 'type': 'object'}🔗
+
+ +
+
+pixelmapSchema = {'additionalProperties': False, 'description': 'A tiled pixelmap to overlay onto a base resource.', 'properties': {'boundaries': {'description': 'True if the pixelmap doubles pixel values such that even values are the fill and odd values the are stroke of each superpixel. If true, the length of the values array should be half of the maximum value in the pixelmap.', 'type': 'boolean'}, 'categories': {'description': 'An array used to map between the values array and color values. Can also contain semantic information for color values.', 'items': {'additionalProperties': False, 'properties': {'description': {'description': 'A more detailed explanation of the meaining of this category.', 'type': 'string'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'label': {'description': 'A string representing the semantic meaning of regions of the map with the corresponding color.', 'type': 'string'}, 'strokeColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}}, 'required': ['fillColor'], 'type': 'object'}, 'type': 'array'}, 'girderId': {'description': 'Girder item ID containing the image to overlay.', 'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'group': {'type': 'string'}, 'hasAlpha': {'description': 'If true, the image is treated assuming it has an alpha channel.', 'type': 'boolean'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'opacity': {'description': 'Default opacity for this image overlay. Must be between 0 and 1. Defaults to 1.', 'maximum': 1, 'minimum': 0, 'type': 'number'}, 'transform': {'description': 'Specification for an affine transform of the image overlay. Includes a 2D transform matrix, an X offset and a Y offset.', 'properties': {'matrix': {'description': 'A 2D matrix representing the transform of an image overlay.', 'items': {'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'xoffset': {'type': 'number'}, 'yoffset': {'type': 'number'}}, 'type': 'object'}, 'type': {'enum': ['pixelmap'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'values': {'description': 'An array where the indices correspond to pixel values in the pixel map image and the values are used to look up the appropriate color in the categories property.', 'items': {'type': 'integer'}, 'type': 'array'}}, 'required': ['boundaries', 'categories', 'girderId', 'type', 'values'], 'type': 'object'}🔗
+
+ +
+
+pointShapeSchema = {'additionalProperties': False, 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'type': {'enum': ['point'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['center', 'type'], 'type': 'object'}🔗
+
+ +
+
+polylineShapeSchema = {'additionalProperties': False, 'properties': {'closed': {'description': 'polyline is open if closed flag is not specified', 'type': 'boolean'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'holes': {'description': 'If closed is true, this is a list of polylines that are treated as holes in the base polygon. These should not cross each other and should be contained within the base polygon.', 'items': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'minItems': 3, 'type': 'array'}, 'type': 'array'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'points': {'items': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'minItems': 2, 'type': 'array'}, 'type': {'enum': ['polyline'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}}, 'required': ['points', 'type'], 'type': 'object'}🔗
+
+ +
+
+rangeValueSchema = {'description': 'A weakly monotonic list of range values', 'items': {'type': 'number'}, 'type': 'array'}🔗
+
+ +
+
+rectangleGridShapeSchema = {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'heightSubdivisions': {'minimum': 1, 'type': 'integer'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['rectanglegrid'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}, 'widthSubdivisions': {'minimum': 1, 'type': 'integer'}}, 'required': ['center', 'height', 'heightSubdivisions', 'type', 'width', 'widthSubdivisions'], 'type': 'object'}🔗
+
+ +
+
+rectangleShapeSchema = {'additionalProperties': False, 'decription': 'normal is the positive z-axis unless otherwise specified', 'properties': {'center': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'fillColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'group': {'type': 'string'}, 'height': {'minimum': 0, 'type': 'number'}, 'id': {'pattern': '^[0-9a-f]{24}$', 'type': 'string'}, 'label': {'additionalProperties': False, 'properties': {'color': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'fontSize': {'exclusiveMinimum': 0, 'type': 'number'}, 'value': {'type': 'string'}, 'visibility': {'enum': ['hidden', 'always', 'onhover'], 'type': 'string'}}, 'required': ['value'], 'type': 'object'}, 'lineColor': {'pattern': '^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$', 'type': 'string'}, 'lineWidth': {'minimum': 0, 'type': 'number'}, 'normal': {'description': 'An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left.', 'items': {'type': 'number'}, 'maxItems': 3, 'minItems': 3, 'name': 'Coordinate', 'type': 'array'}, 'rotation': {'description': 'radians counterclockwise around normal', 'type': 'number'}, 'type': {'enum': ['rectangle'], 'type': 'string'}, 'user': {'additionalProperties': True, 'type': 'object'}, 'width': {'minimum': 0, 'type': 'number'}}, 'required': ['center', 'height', 'type', 'width'], 'type': 'object'}🔗
+
+ +
+
+transformArray = {'description': 'A 2D matrix representing the transform of an image overlay.', 'items': {'maxItems': 2, 'minItems': 2, 'type': 'array'}, 'maxItems': 2, 'minItems': 2, 'type': 'array'}🔗
+
+ +
+
+userSchema = {'additionalProperties': True, 'type': 'object'}🔗
+
+ +
+ +
+
+girder_large_image_annotation.models.annotation.extendSchema(base, add)[source]🔗
+
+ +
+
+

girder_large_image_annotation.models.annotationelement module🔗

+
+
+class girder_large_image_annotation.models.annotationelement.Annotationelement(*args, **kwargs)[source]🔗
+

Bases: Model

+
+
+bboxKeys = {'bottom': ('bbox.lowy', '$lt'), 'details': ('bbox.details', None), 'high': ('bbox.lowz', '$lt'), 'left': ('bbox.highx', '$gte'), 'low': ('bbox.highz', '$gte'), 'minimumSize': ('bbox.size', '$gte'), 'right': ('bbox.lowx', '$lt'), 'size': ('bbox.size', None), 'top': ('bbox.highy', '$gte')}🔗
+
+ +
+
+countElements(annotation)[source]🔗
+
+ +
+
+getElementGroupSet(annotation)[source]🔗
+
+ +
+
+getElements(annotation, region=None)[source]🔗
+

Given an annotation, fetch the elements from the database and add them +to it.

+

When a region is used to request specific element, the following +keys can be specified:

+
+
+
left, right, top, bottom, low, high:
+

the spatial area where +elements are located, all in pixels. If an element’s bounding +box is at least partially within the requested area, that +element is included.

+
+
minimumSize:
+

the minimum size of an element to return.

+
+
sort, sortdir:
+

standard sort options. The sort key can include +size and details.

+
+
limit:
+

limit the total number of elements by this value. Defaults +to no limit.

+
+
offset:
+

the offset within the query to start returning values. If +maxDetails is used, to get subsequent sets of elements, the +offset needs to be increased by the actual number of elements +returned from a previous query, which will vary based on the +details of the elements.

+
+
maxDetails:
+

if specified, limit the total number of elements by +the sum of their details values. This is applied in addition +to limit. The sum of the details values of the elements may +exceed maxDetails slightly (the sum of all but the last element +will be less than maxDetails, but the last element may exceed +the value).

+
+
minElements:
+

if maxDetails is specified, always return this many +elements even if they are very detailed.

+
+
centroids:
+

if specified and true, only return the id, center of +the bounding box, and bounding box size for each element.

+
+
+
+
+
Parameters:
+
    +
  • annotation – the annotation to get elements for. Modified.

  • +
  • region – if present, a dictionary restricting which annotations +are returned.

  • +
+
+
+
+ +
+
+getNextVersionValue()[source]🔗
+

Maintain a version number. This is a single sequence that can be used +to ensure we have the correct set of elements for an annotation.

+
+
Returns:
+

an integer version number that is strictly increasing.

+
+
+
+ +
+
+initialize()[source]🔗
+

Subclasses should override this and set the name of the collection as +self.name. Also, they should set any indexed fields that they require.

+
+ +
+
+removeElements(annotation)[source]🔗
+

Remove all elements related to the specified annotation.

+
+
Parameters:
+

annotation – the annotation to remove elements from.

+
+
+
+ +
+
+removeOldElements(annotation, oldversion=None)[source]🔗
+

Remove all elements related to the specified annotation.

+
+
Parameters:
+
    +
  • annotation – the annotation to remove elements from.

  • +
  • oldversion – if present, remove versions up to this number. If +none, remove versions earlier than the version in +the annotation record.

  • +
+
+
+
+ +
+
+removeWithQuery(query)[source]🔗
+

Remove all documents matching a given query from the collection. +For safety reasons, you may not pass an empty query.

+

Note: this does NOT return a Mongo DeleteResult.

+
+
Parameters:
+

query (dict) – The search query for documents to delete, +see general MongoDB docs for “find()”

+
+
+
+ +
+
+saveElementAsFile(annotation, entries)[source]🔗
+

If an element has a large points or values array, save that array to an +attached file.

+
+
Parameters:
+
    +
  • annotation – the parent annotation.

  • +
  • entries – the database entries document. Modified.

  • +
+
+
+
+ +
+
+updateElementChunk(elements, chunk, chunkSize, annotation, now, insertLock)[source]🔗
+

Update the database for a chunk of elements. See the updateElements +method for details.

+
+ +
+
+updateElements(annotation)[source]🔗
+

Given an annotation, extract the elements from it and update the +database of them.

+
+
Parameters:
+

annotation – the annotation to save elements for. Modified.

+
+
+
+ +
+
+yieldElements(annotation, region=None, info=None, bbox=False)[source]🔗
+

Given an annotation, fetch the elements from the database.

+

When a region is used to request specific element, the following +keys can be specified:

+
+
+
left, right, top, bottom, low, high:
+

the spatial area where +elements are located, all in pixels. If an element’s bounding +box is at least partially within the requested area, that +element is included.

+
+
minimumSize:
+

the minimum size of an element to return.

+
+
sort, sortdir:
+

standard sort options. The sort key can include +size and details.

+
+
limit:
+

limit the total number of elements by this value. Defaults +to no limit.

+
+
offset:
+

the offset within the query to start returning values. If +maxDetails is used, to get subsequent sets of elements, the +offset needs to be increased by the actual number of elements +returned from a previous query, which will vary based on the +details of the elements.

+
+
maxDetails:
+

if specified, limit the total number of elements by +the sum of their details values. This is applied in addition +to limit. The sum of the details values of the elements may +exceed maxDetails slightly (the sum of all but the last element +will be less than maxDetails, but the last element may exceed +the value).

+
+
minElements:
+

if maxDetails is specified, always return this many +elements even if they are very detailed.

+
+
centroids:
+

if specified and true, only return the id, center of +the bounding box, and bounding box size for each element.

+
+
bbox:
+

if specified and true and centroids are not specified, +add _bbox to each element with the bounding box record.

+
+
+
+
+
Parameters:
+
    +
  • annotation – the annotation to get elements for. Modified.

  • +
  • region – if present, a dictionary restricting which annotations +are returned.

  • +
  • info – an optional dictionary that will be modified with +additional query information, including count (total number of +available elements), returned (number of elements in response), +maxDetails (as specified by the region dictionary), details (sum of +details returned), limit (as specified by region), centroids (a +boolean based on the region specification).

  • +
  • bbox – if True, always return bounding box information.

  • +
+
+
Returns:
+

a list of elements. If centroids were requested, each entry +is a list with str(id), x, y, size. Otherwise, each entry is the +element record.

+
+
+
+ +
+ +
+
+

Module contents🔗

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/girder_large_image_annotation/girder_large_image_annotation.rest.html b/_build/girder_large_image_annotation/girder_large_image_annotation.rest.html new file mode 100644 index 000000000..07700b267 --- /dev/null +++ b/_build/girder_large_image_annotation/girder_large_image_annotation.rest.html @@ -0,0 +1,312 @@ + + + + + + + girder_large_image_annotation.rest package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

girder_large_image_annotation.rest package🔗

+
+

Submodules🔗

+
+
+

girder_large_image_annotation.rest.annotation module🔗

+
+
+class girder_large_image_annotation.rest.annotation.AnnotationResource[source]🔗
+

Bases: Resource

+
+
+canCreateFolderAnnotations(folder)[source]🔗
+
+ +
+
+copyAnnotation(annotation, params)[source]🔗
+
+ +
+
+createAnnotation(item, params)[source]🔗
+
+ +
+
+createItemAnnotations(item, annotations)[source]🔗
+
+ +
+
+deleteAnnotation(annotation, params)[source]🔗
+
+ +
+
+deleteFolderAnnotations(id, params)[source]🔗
+
+ +
+
+deleteItemAnnotations(item)[source]🔗
+
+ +
+
+deleteMetadata(annotation, fields)[source]🔗
+
+ +
+
+deleteOldAnnotations(age, versions)[source]🔗
+
+ +
+
+existFolderAnnotations(id, recurse)[source]🔗
+
+ +
+
+find(params)[source]🔗
+
+ +
+
+findAnnotatedImages(params)[source]🔗
+
+ +
+
+getAnnotation(id, params)[source]🔗
+
+ +
+
+getAnnotationAccess(annotation, params)[source]🔗
+
+ +
+
+getAnnotationHistory(id, version)[source]🔗
+
+ +
+
+getAnnotationHistoryList(id, limit, offset, sort)[source]🔗
+
+ +
+
+getAnnotationSchema(params)[source]🔗
+
+ +
+
+getAnnotationWithFormat(annotation, format)[source]🔗
+
+ +
+
+getFolderAnnotations(id, recurse, user, limit=False, offset=False, sort=False, sortDir=False, count=False)[source]🔗
+
+ +
+
+getItemAnnotations(item)[source]🔗
+
+ +
+
+getItemListAnnotationCounts(items)[source]🔗
+
+ +
+
+getItemPlottableData(item, keys, adjacentItems, annotations, requiredKeys, sources=None)[source]🔗
+
+ +
+
+getItemPlottableElements(item, annotations, adjacentItems, sources=None)[source]🔗
+
+ +
+
+getOldAnnotations(age, versions)[source]🔗
+
+ +
+
+returnFolderAnnotations(id, recurse, limit, offset, sort)[source]🔗
+
+ +
+
+revertAnnotationHistory(id, version)[source]🔗
+
+ +
+
+setFolderAnnotationAccess(id, params)[source]🔗
+
+ +
+
+setMetadata(annotation, metadata, allowNull)[source]🔗
+
+ +
+
+updateAnnotation(annotation, params)[source]🔗
+
+ +
+
+updateAnnotationAccess(annotation, params)[source]🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/girder_large_image_annotation/girder_large_image_annotation.utils.html b/_build/girder_large_image_annotation/girder_large_image_annotation.utils.html new file mode 100644 index 000000000..0f308a143 --- /dev/null +++ b/_build/girder_large_image_annotation/girder_large_image_annotation.utils.html @@ -0,0 +1,480 @@ + + + + + + + girder_large_image_annotation.utils package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

girder_large_image_annotation.utils package🔗

+
+

Module contents🔗

+
+
+class girder_large_image_annotation.utils.AnnotationGeoJSON(annotationId, asFeatures=False, mustConvert=False)[source]🔗
+

Bases: object

+

Generate GeoJSON for an annotation via an iterator.

+

Return an itertor for converting an annotation into geojson.

+
+
Parameters:
+
    +
  • annotatioId – the id of the annotation. No permissions checks +are performed.

  • +
  • asFeatures – if False, return a geojson string. If True, return +the features of the geojson. This can be wrapped in +{‘type’: ‘FeatureCollection’, ‘features: […output…]} +to make it a full geojson object.

  • +
  • mustConvert – if True, raise an exception if any annotation +elements cannot be converted. Otherwise, skip those elements.

  • +
+
+
+
+
+circleType(element, geom, prop)[source]🔗
+
+ +
+
+elementToGeoJSON(element)[source]🔗
+
+ +
+
+ellipseType(element, geom, prop)[source]🔗
+
+ +
+
+property geojson🔗
+
+ +
+
+pointType(element, geom, prop)[source]🔗
+
+ +
+
+polylineType(element, geom, prop)[source]🔗
+
+ +
+
+rectangleType(element, geom, prop)[source]🔗
+
+ +
+
+rotate(r, cx, cy, x, y, z)[source]🔗
+
+ +
+ +
+
+class girder_large_image_annotation.utils.GeoJSONAnnotation(geojson)[source]🔗
+

Bases: object

+
+
+property annotation🔗
+
+ +
+
+annotationToJSON()[source]🔗
+
+ +
+
+circleType(elem, result)[source]🔗
+
+ +
+
+property elementCount🔗
+
+ +
+
+property elements🔗
+
+ +
+
+ellipseType(elem, result)[source]🔗
+
+ +
+
+linestringType(elem, result)[source]🔗
+
+ +
+
+multilinestringType(elem, result)[source]🔗
+
+ +
+
+multipointType(elem, result)[source]🔗
+
+ +
+
+multipolygonType(elem, result)[source]🔗
+
+ +
+
+pointType(elem, result)[source]🔗
+
+ +
+
+polygonType(elem, result)[source]🔗
+
+ +
+
+polylineType(elem, result)[source]🔗
+
+ +
+
+rectangleType(elem, result)[source]🔗
+
+ +
+ +
+
+class girder_large_image_annotation.utils.PlottableItemData(user, item, annotations=None, adjacentItems=False, sources=None)[source]🔗
+

Bases: object

+

Get plottable data associated with an item.

+
+
Parameters:
+
    +
  • user – authenticating user.

  • +
  • item – the item record.

  • +
  • annotations – None, a list of annotation ids, or __all__. If +adjacent items are included, the most recent annotation with the +same name will also be included.

  • +
  • adjacentItems – if True, include data from other items in the +same folder. If __all__, include data from other items even if the +data is not present in the current item.

  • +
  • sources – None for all, or a string with a comma-separated list +or a list of strings; when a list, the options are folder, item, +annotation, datafile.

  • +
+
+
+
+
+allowedTypes = (<class 'str'>, <class 'bool'>, <class 'int'>, <class 'float'>)🔗
+
+ +
+
+property columns🔗
+

Get a sorted list of plottable columns with some metadata for each.

+

Each data entry contains

+
+
+
key:
+

the column key. For database entries, this is (item| +annotation|annotationelement).(id|name|description|group| +label). For bounding boxes this is bbox.(x0|y0|x1|y1). For +data from meta / attributes / user, this is +data.(key)[.0][.(key2)][.0]

+
+
type:
+

‘string’ or ‘number’

+
+
title:
+

a human readable title

+
+
count:
+

the number of non-null entries in the column

+
+
[distinct]:
+

a list of distinct values if there are less than some +maximum number of distinct values. This might not include +values from adjacent items

+
+
[distinctcount]:
+

if distinct is populated, this is len(distinct)

+
+
[min]:
+

for number data types, the lowest value present

+
+
[max]:
+

for number data types, the highest value present

+
+
+
+
+
Returns:
+

a sorted list of data entries.

+
+
+
+ +
+
+commonColumns = {'annotation.description': 'Annotation Description', 'annotation.id': 'Annotation ID', 'annotation.name': 'Annotation Name', 'annotationelement.group': 'Annotation Element Group', 'annotationelement.id': 'Annotation Element ID', 'annotationelement.label': 'Annotation Element Label', 'annotationelement.type': 'Annotation Element Type', 'bbox.x0': 'Bounding Box Low X', 'bbox.x1': 'Bounding Box High X', 'bbox.y0': 'Bounding Box Low Y', 'bbox.y1': 'Bounding Box High Y', 'item.description': 'Item Description', 'item.id': 'Item ID', 'item.name': 'Item Name'}🔗
+
+ +
+
+data(columns, requiredColumns=None)[source]🔗
+

Get plottable data.

+
+
Parameters:
+
    +
  • columns – the columns to return. Either a list of column names +or a comma-delimited string.

  • +
  • requiredColumns – only return data rows where all of these +columns are non-None. Either a list of column names of a +comma-delimited string.

  • +
+
+
+
+ +
+
+datafileAnnotationElementSelector(key, cols)[source]🔗
+
+ +
+
+itemNameIDSelector(isName, selector)[source]🔗
+

Given a data selector that returns something that is either an item id, +an item name, or an item name prefix, return the canonical item or +id string from the list of known items.

+
+
Parameters:
+
    +
  • isName – True to return the canonical name, False for the +canonical id.

  • +
  • selector – the selector to get the initial value.

  • +
+
+
Returns:
+

a function that can be used as an overall selector.

+
+
+
+ +
+
+static keySelector(mode, key, key2=None)[source]🔗
+

Given a pattern for getting data from a dictionary, return a selector +that gets that piece of data.

+
+
Parameters:
+
    +
  • mode – one of key, key0, keykey, keykey0, key0key, representing +key lookups in dictionaries or array indices.

  • +
  • key – the first key.

  • +
  • key2 – the second key, if needed.

  • +
+
+
Returns:
+

a pair of functions that can be used to select the value from +the record and data structure. This takes (record, data, row) and +returns a value. The record is the base record used, the data is +the base dictionary, and the row is the location in the index. The +second function takes (record, data) and returns either None or the +number of rows that are present.

+
+
+
+ +
+
+maxAnnotationElements = 5000🔗
+
+ +
+
+maxDistinct = 20🔗
+
+ +
+
+maxItems = 1000🔗
+
+ +
+
+static recordSelector(doctype)[source]🔗
+

Given a document type, return a function that returns the main data +dictionary.

+
+
Parameters:
+

doctype – one of folder, item, annotaiton, annotationelement.

+
+
Returns:
+

a function that takes (record) and returns the data +dictionary, if any.

+
+
+
+ +
+ +
+
+girder_large_image_annotation.utils.isGeoJSON(annotation)[source]🔗
+

Check if a list or dictionary appears to contain a GeoJSON record.

+
+
Parameters:
+

annotation – a list or dictionary.

+
+
Returns:
+

True if this appears to be GeoJSON

+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/girder_large_image_annotation/modules.html b/_build/girder_large_image_annotation/modules.html new file mode 100644 index 000000000..3333f6b48 --- /dev/null +++ b/_build/girder_large_image_annotation/modules.html @@ -0,0 +1,183 @@ + + + + + + + girder_large_image_annotation — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image/large_image.cache_util.html b/_build/large_image/large_image.cache_util.html new file mode 100644 index 000000000..5858bca4f --- /dev/null +++ b/_build/large_image/large_image.cache_util.html @@ -0,0 +1,616 @@ + + + + + + + large_image.cache_util package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image.cache_util package🔗

+
+

Submodules🔗

+
+
+

large_image.cache_util.base module🔗

+
+
+class large_image.cache_util.base.BaseCache(maxsize: float, getsizeof: Callable[[_VT], float] | None = None, **kwargs)[source]🔗
+

Bases: Cache

+

Base interface to cachetools.Cache for use with large-image.

+
+
+clear() None.  Remove all items from D.[source]🔗
+
+ +
+
+property curritems: int🔗
+
+ +
+
+property currsize: int🔗
+

The current size of the cache.

+
+ +
+
+static getCache() Tuple[BaseCache | None, allocate_lock][source]🔗
+
+ +
+
+logError(err: Any, func: Callable, msg: str) None[source]🔗
+

Log errors, but throttle them so as not to spam the logs.

+
+
Parameters:
+
    +
  • err – error to log.

  • +
  • func – function to use for logging. This is something like +logprint.exception or logger.error.

  • +
  • msg – the message to log.

  • +
+
+
+
+ +
+
+property maxsize: int🔗
+

The maximum size of the cache.

+
+ +
+ +
+
+

large_image.cache_util.cache module🔗

+
+
+class large_image.cache_util.cache.LruCacheMetaclass(name, bases, namespace, **kwargs)[source]🔗
+

Bases: type

+
+
+classCaches: Dict[type, Any] = {}🔗
+
+ +
+
+namedCaches: Dict[str, Any] = {}🔗
+
+ +
+ +
+
+large_image.cache_util.cache.getTileCache() Tuple[Cache, allocate_lock | None][source]🔗
+

Get the preferred tile cache and lock.

+
+
Returns:
+

tileCache and tileLock.

+
+
+
+ +
+
+large_image.cache_util.cache.isTileCacheSetup() bool[source]🔗
+

Return True if the tile cache has been created.

+
+
Returns:
+

True if _tileCache is not None.

+
+
+
+ +
+
+large_image.cache_util.cache.methodcache(key: Callable | None = None) Callable[source]🔗
+

Decorator to wrap a function with a memoizing callable that saves results +in self.cache. This is largely taken from cachetools, but uses a cache +from self.cache rather than a passed value. If self.cache_lock is +present and not none, a lock is used.

+
+
Parameters:
+

key – if a function, use that for the key, otherwise use self.wrapKey.

+
+
+
+ +
+
+large_image.cache_util.cache.strhash(*args, **kwargs) str[source]🔗
+

Generate a string hash value for an arbitrary set of args and kwargs. This +relies on the repr of each element.

+
+
Parameters:
+
    +
  • args – arbitrary tuple of args.

  • +
  • kwargs – arbitrary dictionary of kwargs.

  • +
+
+
Returns:
+

hashed string of the arguments.

+
+
+
+ +
+
+

large_image.cache_util.cachefactory module🔗

+
+
+class large_image.cache_util.cachefactory.CacheFactory[source]🔗
+

Bases: object

+
+
+getCache(numItems: int | None = None, cacheName: str | None = None, inProcess: bool = False) Tuple[Cache, allocate_lock | None][source]🔗
+
+ +
+
+getCacheSize(numItems: int | None, cacheName: str | None = None) int[source]🔗
+
+ +
+
+logged = False🔗
+
+ +
+ +
+
+large_image.cache_util.cachefactory.getFirstAvailableCache() Tuple[Cache, allocate_lock | None][source]🔗
+
+ +
+
+large_image.cache_util.cachefactory.loadCaches(entryPointName: str = 'large_image.cache', sourceDict: Dict[str, Type[Cache]] = {}) None[source]🔗
+

Load all caches from entrypoints and add them to the +availableCaches dictionary.

+
+
Parameters:
+
    +
  • entryPointName – the name of the entry points to load.

  • +
  • sourceDict – a dictionary to populate with the loaded caches.

  • +
+
+
+
+ +
+
+large_image.cache_util.cachefactory.pickAvailableCache(sizeEach: int, portion: int = 8, maxItems: int | None = None, cacheName: str | None = None) int[source]🔗
+

Given an estimated size of an item, return how many of those items would +fit in a fixed portion of the available virtual memory.

+
+
Parameters:
+
    +
  • sizeEach – the expected size of an item that could be cached.

  • +
  • portion – the inverse fraction of the memory which can be used.

  • +
  • maxItems – if specified, the number of items is never more than this +value.

  • +
  • cacheName – if specified, the portion can be affected by the +configuration.

  • +
+
+
Returns:
+

the number of items that should be cached. Always at least two, +unless maxItems is less.

+
+
+
+ +
+
+

large_image.cache_util.memcache module🔗

+
+
+class large_image.cache_util.memcache.MemCache(url: str | List[str] = '127.0.0.1', username: str | None = None, password: str | None = None, getsizeof: Callable[[_VT], float] | None = None, mustBeAvailable: bool = False)[source]🔗
+

Bases: BaseCache

+

Use memcached as the backing cache.

+
+
+clear() None.  Remove all items from D.[source]🔗
+
+ +
+
+property curritems: int🔗
+
+ +
+
+property currsize: int🔗
+

The current size of the cache.

+
+ +
+
+static getCache() Tuple[MemCache | None, allocate_lock][source]🔗
+
+ +
+
+property maxsize: int🔗
+

The maximum size of the cache.

+
+ +
+ +
+
+

large_image.cache_util.rediscache module🔗

+
+
+class large_image.cache_util.rediscache.RedisCache(url: str | List[str] = '127.0.0.1:6379', username: str | None = None, password: str | None = None, getsizeof: Callable[[_VT], float] | None = None, mustBeAvailable: bool = False)[source]🔗
+

Bases: BaseCache

+

Use redis as the backing cache.

+
+
+clear() None.  Remove all items from D.[source]🔗
+
+ +
+
+property curritems: int🔗
+
+ +
+
+property currsize: int🔗
+

The current size of the cache.

+
+ +
+
+static getCache() Tuple[RedisCache | None, allocate_lock][source]🔗
+
+ +
+
+property maxsize: int🔗
+

The maximum size of the cache.

+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image.cache_util.CacheFactory[source]🔗
+

Bases: object

+
+
+getCache(numItems: int | None = None, cacheName: str | None = None, inProcess: bool = False) Tuple[Cache, allocate_lock | None][source]🔗
+
+ +
+
+getCacheSize(numItems: int | None, cacheName: str | None = None) int[source]🔗
+
+ +
+
+logged = False🔗
+
+ +
+ +
+
+class large_image.cache_util.LruCacheMetaclass(name, bases, namespace, **kwargs)[source]🔗
+

Bases: type

+
+
+classCaches: Dict[type, Any] = {}🔗
+
+ +
+
+namedCaches: Dict[str, Any] = {}🔗
+
+ +
+ +
+
+class large_image.cache_util.MemCache(url: str | List[str] = '127.0.0.1', username: str | None = None, password: str | None = None, getsizeof: Callable[[_VT], float] | None = None, mustBeAvailable: bool = False)[source]🔗
+

Bases: BaseCache

+

Use memcached as the backing cache.

+
+
+clear() None.  Remove all items from D.[source]🔗
+
+ +
+
+property curritems: int🔗
+
+ +
+
+property currsize: int🔗
+

The current size of the cache.

+
+ +
+
+static getCache() Tuple[MemCache | None, allocate_lock][source]🔗
+
+ +
+
+property maxsize: int🔗
+

The maximum size of the cache.

+
+ +
+ +
+
+class large_image.cache_util.RedisCache(url: str | List[str] = '127.0.0.1:6379', username: str | None = None, password: str | None = None, getsizeof: Callable[[_VT], float] | None = None, mustBeAvailable: bool = False)[source]🔗
+

Bases: BaseCache

+

Use redis as the backing cache.

+
+
+clear() None.  Remove all items from D.[source]🔗
+
+ +
+
+property curritems: int🔗
+
+ +
+
+property currsize: int🔗
+

The current size of the cache.

+
+ +
+
+static getCache() Tuple[RedisCache | None, allocate_lock][source]🔗
+
+ +
+
+property maxsize: int🔗
+

The maximum size of the cache.

+
+ +
+ +
+
+large_image.cache_util.getTileCache() Tuple[Cache, allocate_lock | None][source]🔗
+

Get the preferred tile cache and lock.

+
+
Returns:
+

tileCache and tileLock.

+
+
+
+ +
+
+large_image.cache_util.isTileCacheSetup() bool[source]🔗
+

Return True if the tile cache has been created.

+
+
Returns:
+

True if _tileCache is not None.

+
+
+
+ +
+
+large_image.cache_util.methodcache(key: Callable | None = None) Callable[source]🔗
+

Decorator to wrap a function with a memoizing callable that saves results +in self.cache. This is largely taken from cachetools, but uses a cache +from self.cache rather than a passed value. If self.cache_lock is +present and not none, a lock is used.

+
+
Parameters:
+

key – if a function, use that for the key, otherwise use self.wrapKey.

+
+
+
+ +
+
+large_image.cache_util.pickAvailableCache(sizeEach: int, portion: int = 8, maxItems: int | None = None, cacheName: str | None = None) int[source]🔗
+

Given an estimated size of an item, return how many of those items would +fit in a fixed portion of the available virtual memory.

+
+
Parameters:
+
    +
  • sizeEach – the expected size of an item that could be cached.

  • +
  • portion – the inverse fraction of the memory which can be used.

  • +
  • maxItems – if specified, the number of items is never more than this +value.

  • +
  • cacheName – if specified, the portion can be affected by the +configuration.

  • +
+
+
Returns:
+

the number of items that should be cached. Always at least two, +unless maxItems is less.

+
+
+
+ +
+
+large_image.cache_util.strhash(*args, **kwargs) str[source]🔗
+

Generate a string hash value for an arbitrary set of args and kwargs. This +relies on the repr of each element.

+
+
Parameters:
+
    +
  • args – arbitrary tuple of args.

  • +
  • kwargs – arbitrary dictionary of kwargs.

  • +
+
+
Returns:
+

hashed string of the arguments.

+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image/large_image.html b/_build/large_image/large_image.html new file mode 100644 index 000000000..57588a48d --- /dev/null +++ b/_build/large_image/large_image.html @@ -0,0 +1,748 @@ + + + + + + + large_image package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image package🔗

+
+

Subpackages🔗

+
+ +
+
+
+

Submodules🔗

+
+
+

large_image.config module🔗

+
+
+large_image.config.cpu_count(logical: bool = True) int[source]🔗
+

Get the usable CPU count. If psutil is available, it is used, since it can +determine the number of physical CPUS versus logical CPUs. This returns +the smaller of that value from psutil and the number of cpus allowed by the +os scheduler, which means that for physical requests (logical=False), the +returned value may be more the the number of physical cpus that are usable.

+
+
Parameters:
+

logical – True to get the logical usable CPUs (which include +hyperthreading). False for the physical usable CPUs.

+
+
Returns:
+

the number of usable CPUs.

+
+
+
+ +
+
+large_image.config.getConfig(key: str | None = None, default: str | bool | int | Logger | None = None) Any[source]🔗
+

Get the config dictionary or a value from the cache config settings.

+
+
Parameters:
+
    +
  • key – if None, return the config dictionary. Otherwise, return the +value of the key if it is set or the default value if it is not.

  • +
  • default – a value to return if a key is requested and not set.

  • +
+
+
Returns:
+

either the config dictionary or the value of a key.

+
+
+
+ +
+
+large_image.config.getLogger(key: str | None = None, default: Logger | None = None) Logger[source]🔗
+

Get a logger from the config. Ensure that it is a valid logger.

+
+
Parameters:
+
    +
  • key – if None, return the ‘logger’.

  • +
  • default – a value to return if a key is requested and not set.

  • +
+
+
Returns:
+

a logger.

+
+
+
+ +
+
+large_image.config.minimizeCaching(mode=None)[source]🔗
+

Set python cache sizes to very low values.

+
+
Parameters:
+

mode – None for all caching, ‘tile’ for the tile cache, ‘source’ for +the source cache.

+
+
+
+ +
+
+large_image.config.setConfig(key: str, value: str | bool | int | Logger | None) None[source]🔗
+

Set a value in the config settings.

+
+
Parameters:
+
    +
  • key – the key to set.

  • +
  • value – the value to store in the key.

  • +
+
+
+
+ +
+
+large_image.config.total_memory() int[source]🔗
+

Get the total memory in the system. If this is in a container, try to +determine the memory available to the cgroup.

+
+
Returns:
+

the available memory in bytes, or 8 GB if unknown.

+
+
+
+ +
+
+

large_image.constants module🔗

+
+
+class large_image.constants.SourcePriority(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]🔗
+

Bases: IntEnum

+
+
+FALLBACK = 10🔗
+
+ +
+
+FALLBACK_HIGH = 9🔗
+
+ +
+
+HIGH = 3🔗
+
+ +
+
+HIGHER = 2🔗
+
+ +
+
+IMPLICIT = 8🔗
+
+ +
+
+IMPLICIT_HIGH = 7🔗
+
+ +
+
+LOW = 5🔗
+
+ +
+
+LOWER = 6🔗
+
+ +
+
+MANUAL = 11🔗
+
+ +
+
+MEDIUM = 4🔗
+
+ +
+
+NAMED = 0🔗
+
+ +
+
+PREFERRED = 1🔗
+
+ +
+ +
+
+

large_image.exceptions module🔗

+
+
+exception large_image.exceptions.TileCacheConfigurationError[source]🔗
+

Bases: TileCacheError

+
+ +
+
+exception large_image.exceptions.TileCacheError[source]🔗
+

Bases: TileGeneralError

+
+ +
+
+exception large_image.exceptions.TileGeneralError[source]🔗
+

Bases: Exception

+
+ +
+
+large_image.exceptions.TileGeneralException🔗
+

alias of TileGeneralError

+
+ +
+
+exception large_image.exceptions.TileSourceAssetstoreError[source]🔗
+

Bases: TileSourceError

+
+ +
+
+large_image.exceptions.TileSourceAssetstoreException🔗
+

alias of TileSourceAssetstoreError

+
+ +
+
+exception large_image.exceptions.TileSourceError[source]🔗
+

Bases: TileGeneralError

+
+ +
+
+large_image.exceptions.TileSourceException🔗
+

alias of TileSourceError

+
+ +
+
+exception large_image.exceptions.TileSourceFileNotFoundError(*args, **kwargs)[source]🔗
+

Bases: TileSourceError, FileNotFoundError

+
+ +
+
+exception large_image.exceptions.TileSourceInefficientError[source]🔗
+

Bases: TileSourceError

+
+ +
+
+exception large_image.exceptions.TileSourceXYZRangeError[source]🔗
+

Bases: TileSourceError

+
+ +
+
+

Module contents🔗

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image/large_image.tilesource.html b/_build/large_image/large_image.tilesource.html new file mode 100644 index 000000000..de15b199f --- /dev/null +++ b/_build/large_image/large_image.tilesource.html @@ -0,0 +1,3552 @@ + + + + + + + large_image.tilesource package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image.tilesource package🔗

+
+

Submodules🔗

+
+
+

large_image.tilesource.base module🔗

+
+
+class large_image.tilesource.base.FileTileSource(path: str | Path | Dict[Any, Any], *args, **kwargs)[source]🔗
+

Bases: TileSource

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+classmethod canRead(path: str | Path | Dict[Any, Any], *args, **kwargs) bool[source]🔗
+

Check if we can read the input. This takes the same parameters as +__init__.

+
+
Returns:
+

True if this class can read the input. False if it +cannot.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs) str[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getState() str[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+ +
+
+class large_image.tilesource.base.TileSource(encoding: str = 'JPEG', jpegQuality: int = 95, jpegSubsampling: int = 0, tiffCompression: str = 'raw', edge: bool | str = False, style: str | Dict[str, int] | None = None, noCache: bool | None = None, *args, **kwargs)[source]🔗
+

Bases: IPyLeafletMixin

+

Initialize the tile class.

+
+
Parameters:
+
    +
  • jpegQuality – when serving jpegs, use this quality.

  • +
  • jpegSubsampling – when serving jpegs, use this subsampling (0 is +full chroma, 1 is half, 2 is quarter).

  • +
  • encoding – ‘JPEG’, ‘PNG’, ‘TIFF’, or ‘TILED’.

  • +
  • edge – False to leave edge tiles whole, True or ‘crop’ to crop +edge tiles, otherwise, an #rrggbb color to fill edges.

  • +
  • tiffCompression – the compression format to use when encoding a +TIFF.

  • +
  • style

    if None, use the default style for the file. Otherwise, +this is a string with a json-encoded dictionary. The style can +contain the following keys:

    +
    +
    +
    band:
    +

    if -1 or None, and if style is specified at all, the +greyscale value is used. Otherwise, a 1-based numerical +index into the channels of the image or a string that +matches the interpretation of the band (‘red’, ‘green’, +‘blue’, ‘gray’, ‘alpha’). Note that ‘gray’ on an RGB or +RGBA image will use the green band.

    +
    +
    frame:
    +

    if specified, override the frame value for this band. +When used as part of a bands list, this can be used to +composite multiple frames together. It is most efficient +if at least one band either doesn’t specify a frame +parameter or specifies the same frame value as the primary +query.

    +
    +
    framedelta:
    +

    if specified and frame is not specified, override +the frame value for this band by using the current frame +plus this value.

    +
    +
    min:
    +

    the value to map to the first palette value. Defaults to +0. ‘auto’ to use 0 if the reported minimum and maximum of +the band are between [0, 255] or use the reported minimum +otherwise. ‘min’ or ‘max’ to always uses the reported +minimum or maximum. ‘full’ to always use 0.

    +
    +
    max:
    +

    the value to map to the last palette value. Defaults to +255. ‘auto’ to use 0 if the reported minimum and maximum +of the band are between [0, 255] or use the reported +maximum otherwise. ‘min’ or ‘max’ to always uses the +reported minimum or maximum. ‘full’ to use the maximum +value of the base data type (either 1, 255, or 65535).

    +
    +
    palette:
    +

    a single color string, a palette name, or a list of +two or more color strings. Color strings are of the form +#RRGGBB, #RRGGBBAA, #RGB, #RGBA, or any string parseable by +the PIL modules, or, if it is installed, by matplotlib. A +single color string is the same as the list [‘#000’, +<color>]. Palette names are the name of a palettable +palette or, if available, a matplotlib palette.

    +
    +
    nodata:
    +

    the value to use for missing data. null or unset to +not use a nodata value.

    +
    +
    composite:
    +

    either ‘lighten’ or ‘multiply’. Defaults to +‘lighten’ for all except the alpha band.

    +
    +
    clamp:
    +

    either True to clamp (also called clip or crop) values +outside of the [min, max] to the ends of the palette or +False to make outside values transparent.

    +
    +
    dtype:
    +

    convert the results to the specified numpy dtype. +Normally, if a style is applied, the results are +intermediately a float numpy array with a value range of +[0,255]. If this is ‘uint16’, it will be cast to that and +multiplied by 65535/255. If ‘float’, it will be divided by +255. If ‘source’, this uses the dtype of the source image.

    +
    +
    axis:
    +

    keep only the specified axis from the numpy intermediate +results. This can be used to extract a single channel +after compositing.

    +
    +
    +
    +

    Alternately, the style object can contain a single key of ‘bands’, +which has a value which is a list of style dictionaries as above, +excepting that each must have a band that is not -1. Bands are +composited in the order listed. This base object may also contain +the ‘dtype’ and ‘axis’ values.

    +

  • +
  • noCache – if True, the style can be adjusted dynamically and the +source is not elibible for caching. If there is no intention to +reuse the source at a later time, this can have performance +benefits, such as when first cataloging images that can be read.

  • +
+
+
+
+
+property bandCount: int | None🔗
+
+ +
+
+classmethod canRead(*args, **kwargs)[source]🔗
+

Check if we can read the input. This takes the same parameters as +__init__.

+
+
Returns:
+

True if this class can read the input. False if it cannot.

+
+
+
+ +
+
+convertRegionScale(sourceRegion: Dict[str, Any], sourceScale: Dict[str, float] | None = None, targetScale: Dict[str, float] | None = None, targetUnits: str | None = None, cropToImage: bool = True) Dict[str, Any][source]🔗
+

Convert a region from one scale to another.

+
+
Parameters:
+
    +
  • sourceRegion

    a dictionary of optional values which specify the +part of an image to process.

    +
    +
    left:
    +

    the left edge (inclusive) of the region to process.

    +
    +
    top:
    +

    the top edge (inclusive) of the region to process.

    +
    +
    right:
    +

    the right edge (exclusive) of the region to process.

    +
    +
    bottom:
    +

    the bottom edge (exclusive) of the region to process.

    +
    +
    width:
    +

    the width of the region to process.

    +
    +
    height:
    +

    the height of the region to process.

    +
    +
    units:
    +

    either ‘base_pixels’ (default), ‘pixels’, ‘mm’, or +‘fraction’. base_pixels are in maximum resolution pixels. +pixels is in the specified magnification pixels. mm is in the +specified magnification scale. fraction is a scale of 0 to 1. +pixels and mm are only available if the magnification and mm +per pixel are defined for the image.

    +
    +
    +

  • +
  • sourceScale

    a dictionary of optional values which specify the +scale of the source region. Required if the sourceRegion is +in “mag_pixels” units.

    +
    +
    magnification:
    +

    the magnification ratio.

    +
    +
    mm_x:
    +

    the horizontal size of a pixel in millimeters.

    +
    +
    mm_y:
    +

    the vertical size of a pixel in millimeters.

    +
    +
    +

  • +
  • targetScale

    a dictionary of optional values which specify the +scale of the target region. Required in targetUnits is in +“mag_pixels” units.

    +
    +
    magnification:
    +

    the magnification ratio.

    +
    +
    mm_x:
    +

    the horizontal size of a pixel in millimeters.

    +
    +
    mm_y:
    +

    the vertical size of a pixel in millimeters.

    +
    +
    +

  • +
  • targetUnits – if not None, convert the region to these units. +Otherwise, the units are will either be the sourceRegion units if +those are not “mag_pixels” or base_pixels. If “mag_pixels”, the +targetScale must be specified.

  • +
  • cropToImage – if True, don’t return region coordinates outside of +the image.

  • +
+
+
+
+ +
+
+property dtype: dtype🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {None: SourcePriority.FALLBACK}🔗
+
+ +
+
+property frames: int🔗
+

A property with the number of frames.

+
+ +
+
+property geospatial: bool🔗
+
+ +
+
+getAssociatedImage(imageKey: str, *args, **kwargs) Tuple[ImageBytes, str] | None[source]🔗
+

Return an associated image.

+
+
Parameters:
+
    +
  • imageKey – the key of the associated image to retrieve.

  • +
  • kwargs – optional arguments. Some options are width, height, +encoding, jpegQuality, jpegSubsampling, and tiffCompression.

  • +
+
+
Returns:
+

imageData, imageMime: the image data and the mime type, or +None if the associated image doesn’t exist.

+
+
+
+ +
+
+getAssociatedImagesList() List[str][source]🔗
+

Return a list of associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getBandInformation(statistics: bool = False, **kwargs) Dict[int, Any][source]🔗
+

Get information about each band in the image.

+
+
Parameters:
+

statistics – if True, compute statistics if they don’t already +exist.

+
+
Returns:
+

a dictionary of one dictionary per band. Each dictionary +contains known values such as interpretation, min, max, mean, +stdev.

+
+
+
+ +
+
+getBounds(*args, **kwargs) Dict[str, Any][source]🔗
+
+ +
+
+getCenter(*args, **kwargs) Tuple[float, float][source]🔗
+

Returns (Y, X) center location.

+
+ +
+
+getICCProfiles(idx: int | None = None, onlyInfo: bool = False) ImageCmsProfile | List[ImageCmsProfile | None] | None[source]🔗
+

Get a list of all ICC profiles that are available for the source, or +get a specific profile.

+
+
Parameters:
+
    +
  • idx – a 0-based index into the profiles to get one profile, or +None to get a list of all profiles.

  • +
  • onlyInfo – if idx is None and this is true, just return the +profile information.

  • +
+
+
Returns:
+

either one or a list of PIL.ImageCms.CmsProfile objects, or +None if no profiles are available. If a list, entries in the list +may be None.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs) str[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getLevelForMagnification(magnification: float | None = None, exact: bool = False, mm_x: float | None = None, mm_y: float | None = None, rounding: str | bool | None = 'round', **kwargs) int | float | None[source]🔗
+

Get the level for a specific magnification or pixel size. If the +magnification is unknown or no level is sufficient resolution, and an +exact match is not requested, the highest level will be returned.

+

If none of magnification, mm_x, and mm_y are specified, the maximum +level is returned. If more than one of these values is given, an +average of those given will be used (exact will require all of them to +match).

+
+
Parameters:
+
    +
  • magnification – the magnification ratio.

  • +
  • exact – if True, only a level that matches exactly will be +returned.

  • +
  • mm_x – the horizontal size of a pixel in millimeters.

  • +
  • mm_y – the vertical size of a pixel in millimeters.

  • +
  • rounding – if False, a fractional level may be returned. If +‘ceil’ or ‘round’, that function is used to convert the level to an +integer (the exact flag still applies). If None, the level is not +cropped to the actual image’s level range.

  • +
+
+
Returns:
+

the selected level or None for no match.

+
+
+
+ +
+
+getMagnificationForLevel(level: float | None = None) Dict[str, float | None][source]🔗
+

Get the magnification at a particular level.

+
+
Parameters:
+

level – None to use the maximum level, otherwise the level to get +the magnification factor of.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getMetadata() JSONDict[source]🔗
+

Return metadata about this tile source. This contains

+
+
+
levels:
+

number of tile levels in this image.

+
+
sizeX:
+

width of the image in pixels.

+
+
sizeY:
+

height of the image in pixels.

+
+
tileWidth:
+

width of a tile in pixels.

+
+
tileHeight:
+

height of a tile in pixels.

+
+
magnification:
+

if known, the magnificaiton of the image.

+
+
mm_x:
+

if known, the width of a pixel in millimeters.

+
+
mm_y:
+

if known, the height of a pixel in millimeters.

+
+
dtype:
+

if known, the type of values in this image.

+
+
+

In addition to the keys that listed above, tile sources that expose +multiple frames will also contain

+
+
frames:
+

a list of frames. Each frame entry is a dictionary with

+
+
Frame:
+

a 0-values frame index (the location in the list)

+
+
Channel:
+

optional. The name of the channel, if known

+
+
IndexC:
+

optional if unique. A 0-based index into the channel +list

+
+
IndexT:
+

optional if unique. A 0-based index for time values

+
+
IndexZ:
+

optional if unique. A 0-based index for z values

+
+
IndexXY:
+

optional if unique. A 0-based index for view (xy) +values

+
+
Index<axis>:
+

optional if unique. A 0-based index for an +arbitrary axis.

+
+
Index:
+

a 0-based index of non-channel unique sets. If the +frames vary only by channel and are adjacent, they will +have the same index.

+
+
+
+
IndexRange:
+

a dictionary of the number of unique index values from +frames if greater than 1 (e.g., if an entry like IndexXY is not +present, then all frames either do not have that value or have +a value of 0).

+
+
IndexStride:
+

a dictionary of the spacing between frames where +unique axes values change.

+
+
channels:
+

optional. If known, a list of channel names

+
+
channelmap:
+

optional. If known, a dictionary of channel names +with their offset into the channel list.

+
+
+
+

Note that this does not include band information, though some tile +sources may do so.

+
+ +
+
+getNativeMagnification() Dict[str, float | None][source]🔗
+

Get the magnification for the highest-resolution level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getOneBandInformation(band: int) Dict[str, Any][source]🔗
+

Get band information for a single band.

+
+
Parameters:
+

band – a 1-based band.

+
+
Returns:
+

a dictionary of band information. See getBandInformation.

+
+
+
+ +
+
+getPixel(includeTileRecord: bool = False, **kwargs) JSONDict[source]🔗
+

Get a single pixel from the current tile source.

+
+
Parameters:
+
    +
  • includeTileRecord – if True, include the tile used for computing +the pixel in the response.

  • +
  • kwargs – optional arguments. Some options are region, output, +encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See +tileIterator.

  • +
+
+
Returns:
+

a dictionary with the value of the pixel for each channel on +a scale of [0-255], including alpha, if available. This may +contain additional information.

+
+
+
+ +
+
+getPointAtAnotherScale(point: Tuple[float, float], sourceScale: Dict[str, float] | None = None, sourceUnits: str | None = None, targetScale: Dict[str, float] | None = None, targetUnits: str | None = None, **kwargs) Tuple[float, float][source]🔗
+

Given a point as a (x, y) tuple, convert it from one scale to another. +The sourceScale, sourceUnits, targetScale, and targetUnits parameters +are the same as convertRegionScale, where sourceUnits are the units +used with sourceScale.

+
+ +
+
+getPreferredLevel(level: int) int[source]🔗
+

Given a desired level (0 is minimum resolution, self.levels - 1 is max +resolution), return the level that contains actual data that is no +lower resolution.

+
+
Parameters:
+

level – desired level

+
+
Returns level:
+

a level with actual data that is no lower resolution.

+
+
+
+ +
+
+getRegion(format: str | Tuple[str] = ('image',), **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

Get a rectangular region from the current tile source. Aspect ratio is +preserved. If neither width nor height is given, the original size of +the highest resolution level is used. If both are given, the returned +image will be no larger than either size.

+
+
Parameters:
+
    +
  • format – the desired format or a tuple of allowed formats. +Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, +TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding may be +specified.

  • +
  • kwargs – optional arguments. Some options are region, output, +encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See +tileIterator.

  • +
+
+
Returns:
+

regionData, formatOrRegionMime: the image data and either the +mime type, if the format is TILE_FORMAT_IMAGE, or the format.

+
+
+
+ +
+
+getRegionAtAnotherScale(sourceRegion: Dict[str, Any], sourceScale: Dict[str, float] | None = None, targetScale: Dict[str, float] | None = None, targetUnits: str | None = None, **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

This takes the same parameters and returns the same results as +getRegion, except instead of region and scale, it takes sourceRegion, +sourceScale, targetScale, and targetUnits. These parameters are the +same as convertRegionScale. See those two functions for parameter +definitions.

+
+ +
+
+getSingleTile(*args, **kwargs) LazyTileDict | None[source]🔗
+

Return any single tile from an iterator. This takes exactly the same +parameters as tileIterator. Use tile_position to get a specific tile, +otherwise the first tile is returned.

+
+
Returns:
+

a tile dictionary or None.

+
+
+
+ +
+
+getSingleTileAtAnotherScale(*args, **kwargs) LazyTileDict | None[source]🔗
+

Return any single tile from a rescaled iterator. This takes exactly +the same parameters as tileIteratorAtAnotherScale. Use tile_position +to get a specific tile, otherwise the first tile is returned.

+
+
Returns:
+

a tile dictionary or None.

+
+
+
+ +
+
+getState() str[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getThumbnail(width: str | int | None = None, height: str | int | None = None, **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

Get a basic thumbnail from the current tile source. Aspect ratio is +preserved. If neither width nor height is given, a default value is +used. If both are given, the thumbnail will be no larger than either +size. A thumbnail has the same options as a region except that it +always includes the entire image and has a default size of 256 x 256.

+
+
Parameters:
+
    +
  • width – maximum width in pixels.

  • +
  • height – maximum height in pixels.

  • +
  • kwargs – optional arguments. Some options are encoding, +jpegQuality, jpegSubsampling, and tiffCompression.

  • +
+
+
Returns:
+

thumbData, thumbMime: the image data and the mime type.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, sparseFallback=False, frame=None)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+getTileCount(*args, **kwargs) int[source]🔗
+

Return the number of tiles that the tileIterator will return. See +tileIterator for parameters.

+
+
Returns:
+

the number of tiles that the tileIterator will yield.

+
+
+
+ +
+
+getTileMimeType() str[source]🔗
+

Return the default mimetype for image tiles.

+
+
Returns:
+

the mime type of the tile.

+
+
+
+ +
+
+histogram(dtype: dtype[Any] | None | type[Any] | _SupportsDType[dtype[Any]] | str | tuple[Any, int] | tuple[Any, SupportsIndex | Sequence[SupportsIndex]] | list[Any] | _DTypeDict | tuple[Any, Any] = None, onlyMinMax: bool = False, bins: int = 256, density: bool = False, format: Any = None, *args, **kwargs) Dict[str, ndarray | List[Dict[str, Any]]][source]🔗
+

Get a histogram for a region.

+
+
Parameters:
+
    +
  • dtype – if specified, the tiles must be this numpy.dtype.

  • +
  • onlyMinMax – if True, only return the minimum and maximum value +of the region.

  • +
  • bins – the number of bins in the histogram. This is passed to +numpy.histogram, but needs to produce the same set of edges for +each tile.

  • +
  • density – if True, scale the results based on the number of +samples.

  • +
  • format – ignored. Used to override the format for the +tileIterator.

  • +
  • range – if None, use the computed min and (max + 1). Otherwise, +this is the range passed to numpy.histogram. Note this is only +accessible via kwargs as it otherwise overloads the range function. +If ‘round’, use the computed values, but the number of bins may be +reduced or the bin_edges rounded to integer values for +integer-based source data.

  • +
  • args – parameters to pass to the tileIterator.

  • +
  • kwargs – parameters to pass to the tileIterator.

  • +
+
+
Returns:
+

if onlyMinMax is true, this is a dictionary with keys min and +max, each of which is a numpy array with the minimum and maximum of +all of the bands. If onlyMinMax is False, this is a dictionary +with a single key ‘histogram’ that contains a list of histograms +per band. Each entry is a dictionary with min, max, range, hist, +bins, and bin_edges. range is [min, (max + 1)]. hist is the +counts (normalized if density is True) for each bin. bins is the +number of bins used. bin_edges is an array one longer than the +hist array that contains the boundaries between bins.

+
+
+
+ +
+
+property metadata: JSONDict🔗
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = None🔗
+
+ +
+
+nameMatches: Dict[str, SourcePriority] = {}🔗
+
+ +
+
+newPriority: SourcePriority | None = None🔗
+
+ +
+
+property style🔗
+
+ +
+
+tileFrames(format: str | Tuple[str] = ('image',), frameList: List[int] | None = None, framesAcross: int | None = None, max_workers: int | None = -4, **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

Given the parameters for getRegion, plus a list of frames and the +number of frames across, make a larger image composed of a region from +each listed frame composited together.

+
+
Parameters:
+
    +
  • format – the desired format or a tuple of allowed formats. +Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, +TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding may be +specified.

  • +
  • frameList – None for all frames, or a list of 0-based integers.

  • +
  • framesAcross – the number of frames across the final image. If +unspecified, this is the ceiling of sqrt(number of frames in frame +list).

  • +
  • kwargs – optional arguments. Some options are region, output, +encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See +tileIterator.

  • +
  • max_workers – maximum workers for parallelism. If negative, use +the minimum of the absolute value of this number or +multiprocessing.cpu_count().

  • +
+
+
Returns:
+

regionData, formatOrRegionMime: the image data and either the +mime type, if the format is TILE_FORMAT_IMAGE, or the format.

+
+
+
+ +
+
+tileIterator(format: str | Tuple[str] = ('numpy',), resample: bool = True, **kwargs) Iterator[LazyTileDict][source]🔗
+

Iterate on all tiles in the specified region at the specified scale. +Each tile is returned as part of a dictionary that includes

+
+
+
x, y:
+

(left, top) coordinates in current magnification pixels

+
+
width, height:
+

size of current tile in current magnification pixels

+
+
tile:
+

cropped tile image

+
+
format:
+

format of the tile

+
+
level:
+

level of the current tile

+
+
level_x, level_y:
+

the tile reference number within the level. +Tiles are numbered (0, 0), (1, 0), (2, 0), etc. The 0th tile +yielded may not be (0, 0) if a region is specified.

+
+
tile_position:
+

a dictionary of the tile position within the +iterator, containing:

+
+
level_x, level_y:
+

the tile reference number within the level.

+
+
region_x, region_y:
+

0, 0 is the first tile in the full +iteration (when not restricting the iteration to a single +tile).

+
+
position:
+

a 0-based value for the tile within the full +iteration.

+
+
+
+
iterator_range:
+

a dictionary of the output range of the iterator:

+
+
level_x_min, level_x_max:
+

the tiles that are be included +during the full iteration: [layer_x_min, layer_x_max).

+
+
level_y_min, level_y_max:
+

the tiles that are be included +during the full iteration: [layer_y_min, layer_y_max).

+
+
region_x_max, region_y_max:
+

the number of tiles included during +the full iteration. This is layer_x_max - layer_x_min, +layer_y_max - layer_y_min.

+
+
position:
+

the total number of tiles included in the full +iteration. This is region_x_max * region_y_max.

+
+
+
+
magnification:
+

magnification of the current tile

+
+
mm_x, mm_y:
+

size of the current tile pixel in millimeters.

+
+
gx, gy:
+

(left, top) coordinates in maximum-resolution pixels

+
+
gwidth, gheight:
+

size of of the current tile in maximum-resolution +pixels.

+
+
tile_overlap:
+

the amount of overlap with neighboring tiles (left, +top, right, and bottom). Overlap never extends outside of the +requested region.

+
+
+
+

If a region that includes partial tiles is requested, those tiles are +cropped appropriately. Most images will have tiles that get cropped +along the right and bottom edges in any case. If an exact +magnification or scale is requested, no tiles will be returned.

+
+
Parameters:
+
    +
  • format – the desired format or a tuple of allowed formats. +Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, +TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding must be +specified.

  • +
  • resample

    If True or one of PIL.Image.Resampling.NEAREST, +LANCZOS, BILINEAR, or BICUBIC to resample tiles that are not the +target output size. Tiles that are resampled will have additional +dictionary entries of:

    +
    +
    scaled:
    +

    the scaling factor that was applied (less than 1 is +downsampled).

    +
    +
    tile_x, tile_y:
    +

    (left, top) coordinates before scaling

    +
    +
    tile_width, tile_height:
    +

    size of the current tile before +scaling.

    +
    +
    tile_magnification:
    +

    magnification of the current tile before +scaling.

    +
    +
    tile_mm_x, tile_mm_y:
    +

    size of a pixel in a tile in millimeters +before scaling.

    +
    +
    +

    Note that scipy.misc.imresize uses PIL internally.

    +

  • +
  • region

    a dictionary of optional values which specify the part +of the image to process:

    +
    +
    left:
    +

    the left edge (inclusive) of the region to process.

    +
    +
    top:
    +

    the top edge (inclusive) of the region to process.

    +
    +
    right:
    +

    the right edge (exclusive) of the region to process.

    +
    +
    bottom:
    +

    the bottom edge (exclusive) of the region to process.

    +
    +
    width:
    +

    the width of the region to process.

    +
    +
    height:
    +

    the height of the region to process.

    +
    +
    units:
    +

    either ‘base_pixels’ (default), ‘pixels’, ‘mm’, or +‘fraction’. base_pixels are in maximum resolution pixels. +pixels is in the specified magnification pixels. mm is in the +specified magnification scale. fraction is a scale of 0 to 1. +pixels and mm are only available if the magnification and mm +per pixel are defined for the image.

    +
    +
    +

  • +
  • output

    a dictionary of optional values which specify the size +of the output.

    +
    +
    maxWidth:
    +

    maximum width in pixels. If either maxWidth or maxHeight +is specified, magnification, mm_x, and mm_y are ignored.

    +
    +
    maxHeight:
    +

    maximum height in pixels.

    +
    +
    +

  • +
  • scale

    a dictionary of optional values which specify the scale +of the region and / or output. This applies to region if +pixels or mm are used for inits. It applies to output if +neither output maxWidth nor maxHeight is specified.

    +
    +
    magnification:
    +

    the magnification ratio. Only used if maxWidth and +maxHeight are not specified or None.

    +
    +
    mm_x:
    +

    the horizontal size of a pixel in millimeters.

    +
    +
    mm_y:
    +

    the vertical size of a pixel in millimeters.

    +
    +
    exact:
    +

    if True, only a level that matches exactly will be returned. +This is only applied if magnification, mm_x, or mm_y is used.

    +
    +
    +

  • +
  • tile_position – if present, either a number to only yield the +(tile_position)th tile [0 to (xmax - min) * (ymax - ymin)) that the +iterator would yield, or a dictionary of {region_x, region_y} to +yield that tile, where 0, 0 is the first tile yielded, and +xmax - xmin - 1, ymax - ymin - 1 is the last tile yielded, or a +dictionary of {level_x, level_y} to yield that specific tile if it +is in the region.

  • +
  • tile_size

    if present, retile the output to the specified tile +size. If only width or only height is specified, the resultant +tiles will be square. This is a dictionary containing at least +one of:

    +
    +
    width:
    +

    the desired tile width.

    +
    +
    height:
    +

    the desired tile height.

    +
    +
    +

  • +
  • tile_overlap

    if present, retile the output adding a symmetric +overlap to the tiles. If either x or y is not specified, it +defaults to zero. The overlap does not change the tile size, +only the stride of the tiles. This is a dictionary containing:

    +
    +
    x:
    +

    the horizontal overlap in pixels.

    +
    +
    y:
    +

    the vertical overlap in pixels.

    +
    +
    edges:
    +

    if True, then the edge tiles will exclude the overlap +distance. If unset or False, the edge tiles are full size.

    +

    The overlap is conceptually split between the two sides of +the tile. This is only relevant to where overlap is reported +or if edges is True

    +

    As an example, suppose an image that is 8 pixels across +(01234567) and a tile size of 5 is requested with an overlap of +4. If the edges option is False (the default), the following +tiles are returned: 01234, 12345, 23456, 34567. Each tile +reports its overlap, and the non-overlapped area of each tile +is 012, 3, 4, 567. If the edges option is True, the tiles +returned are: 012, 0123, 01234, 12345, 23456, 34567, 4567, 567, +with the non-overlapped area of each as 0, 1, 2, 3, 4, 5, 6, 7.

    +
    +
    +

  • +
  • tile_offset

    if present, adjust tile positions so that the +corner of one tile is at the specified location.

    +
    +
    left:
    +

    the left offset in pixels.

    +
    +
    top:
    +

    the top offset in pixels.

    +
    +
    auto:
    +

    a boolean, if True, automatically set the offset to align +with the region’s left and top.

    +
    +
    +

  • +
  • encoding – if format includes TILE_FORMAT_IMAGE, a valid PIL +encoding (typically ‘PNG’, ‘JPEG’, or ‘TIFF’) or ‘TILED’ (identical +to TIFF). Must also be in the TileOutputMimeTypes map.

  • +
  • jpegQuality – the quality to use when encoding a JPEG.

  • +
  • jpegSubsampling – the subsampling level to use when encoding a +JPEG.

  • +
  • tiffCompression – the compression format when encoding a TIFF. +This is usually ‘raw’, ‘tiff_lzw’, ‘jpeg’, or ‘tiff_adobe_deflate’. +Some of these are aliased: ‘none’, ‘lzw’, ‘deflate’.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
  • kwargs – optional arguments.

  • +
+
+
Yields:
+

an iterator that returns a dictionary as listed above.

+
+
+
+ +
+
+tileIteratorAtAnotherScale(sourceRegion: Dict[str, Any], sourceScale: Dict[str, float] | None = None, targetScale: Dict[str, float] | None = None, targetUnits: str | None = None, **kwargs) Iterator[LazyTileDict][source]🔗
+

This takes the same parameters and returns the same results as +tileIterator, except instead of region and scale, it takes +sourceRegion, sourceScale, targetScale, and targetUnits. These +parameters are the same as convertRegionScale. See those two functions +for parameter definitions.

+
+ +
+
+wrapKey(*args, **kwargs) str[source]🔗
+

Return a key for a tile source and function parameters that can be used +as a unique cache key.

+
+
Parameters:
+
    +
  • args – arguments to add to the hash.

  • +
  • kwaths – arguments to add to the hash.

  • +
+
+
Returns:
+

a cache key.

+
+
+
+ +
+ +
+
+

large_image.tilesource.geo module🔗

+
+
+class large_image.tilesource.geo.GDALBaseFileTileSource(path: str | Path | Dict[Any, Any], *args, **kwargs)[source]🔗
+

Bases: GeoBaseFileTileSource

+

Abstract base class for GDAL-based tile sources.

+

This base class assumes the underlying library is powered by GDAL +(rasterio, mapnik, etc.)

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+extensions: Dict[str | None, SourcePriority] = {'geotiff': SourcePriority.PREFERRED, 'nitf': SourcePriority.PREFERRED, 'ntf': SourcePriority.PREFERRED, 'tif': SourcePriority.LOW, 'tiff': SourcePriority.LOW, 'vrt': SourcePriority.PREFERRED, None: SourcePriority.MEDIUM}🔗
+
+ +
+
+property geospatial: bool🔗
+

This is true if the source has geospatial information.

+
+ +
+
+getBounds(*args, **kwargs) Dict[str, Any][source]🔗
+
+ +
+
+static getHexColors(palette: str | List[str | float | Tuple[float, ...]]) List[str][source]🔗
+

Returns list of hex colors for a given color palette

+
+
Returns:
+

List of colors

+
+
+
+ +
+
+getNativeMagnification() Dict[str, float | None][source]🔗
+

Get the magnification at the base level.

+
+
Returns:
+

width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getPixelSizeInMeters() float | None[source]🔗
+

Get the approximate base pixel size in meters. This is calculated as +the average scale of the four edges in the WGS84 ellipsoid.

+
+
Returns:
+

the pixel size in meters or None.

+
+
+
+ +
+
+getThumbnail(width: str | int | None = None, height: str | int | None = None, **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

Get a basic thumbnail from the current tile source. Aspect ratio is +preserved. If neither width nor height is given, a default value is +used. If both are given, the thumbnail will be no larger than either +size. A thumbnail has the same options as a region except that it +always includes the entire image if there is no projection and has a +default size of 256 x 256.

+
+
Parameters:
+
    +
  • width – maximum width in pixels.

  • +
  • height – maximum height in pixels.

  • +
  • kwargs – optional arguments. Some options are encoding, +jpegQuality, jpegSubsampling, and tiffCompression.

  • +
+
+
Returns:
+

thumbData, thumbMime: the image data and the mime type.

+
+
+
+ +
+
+getTileCorners(z: int, x: float, y: float) Tuple[float, float, float, float][source]🔗
+

Returns bounds of a tile for a given x,y,z index.

+
+
Parameters:
+
    +
  • z – tile level

  • +
  • x – tile offset from left.

  • +
  • y – tile offset from right

  • +
+
+
Returns:
+

(xmin, ymin, xmax, ymax) in the current projection or base +pixels.

+
+
+
+ +
+
+static isGeospatial(path: str | Path) bool[source]🔗
+

Check if a path is likely to be a geospatial file.

+
+
Parameters:
+

path – The path to the file

+
+
Returns:
+

True if geospatial.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/geotiff': SourcePriority.PREFERRED, 'image/tiff': SourcePriority.LOW, 'image/x-tiff': SourcePriority.LOW, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+pixelToProjection(*args, **kwargs) Tuple[float, float][source]🔗
+
+ +
+
+projection: str | bytes🔗
+
+ +
+
+projectionOrigin: Tuple[float, float]🔗
+
+ +
+
+sourceLevels: int🔗
+
+ +
+
+sourceSizeX: int🔗
+
+ +
+
+sourceSizeY: int🔗
+
+ +
+
+toNativePixelCoordinates(*args, **kwargs) Tuple[float, float][source]🔗
+
+ +
+
+unitsAcrossLevel0: float🔗
+
+ +
+ +
+
+class large_image.tilesource.geo.GeoBaseFileTileSource(path: str | Path | Dict[Any, Any], *args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Abstract base class for geospatial tile sources.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+ +
+
+large_image.tilesource.geo.make_vsi(url: str | Path | Dict[Any, Any], **options) str[source]🔗
+
+ +
+
+

large_image.tilesource.jupyter module🔗

+

A vanilla REST interface to a TileSource.

+

This is intended for use in JupyterLab and not intended to be used as a full +fledged REST API. Only two endpoints are exposed with minimal options:

+
    +
  • /metadata

  • +
  • /tile?z={z}&x={x}&y={y}&encoding=png

  • +
+

We use Tornado because it is Jupyter’s web server and will not require Jupyter +users to install any additional dependencies. Also, Tornado doesn’t require us +to manage a separate thread for the web server.

+

Please note that this webserver will not work with Classic Notebook and will +likely lead to crashes. This is only for use in JupyterLab.

+
+
+class large_image.tilesource.jupyter.IPyLeafletMixin(*args, **kwargs)[source]🔗
+

Bases: object

+

Mixin class to support interactive visualization in JupyterLab.

+

This class implements _ipython_display_ with ipyleaflet +to display an interactive image visualizer for the tile source +in JupyterLab.

+

Install ipyleaflet +to interactively visualize tile sources in JupyterLab.

+

For remote JupyterHub environments, you may need to configure +the class variables JUPYTER_HOST or JUPYTER_PROXY.

+

If JUPYTER_PROXY is set, it overrides JUPYTER_HOST.

+

Use JUPYTER_HOST to set the host name of the machine such +that the tile URL can be accessed at +'http://{JUPYTER_HOST}:{port}'.

+

Use JUPYTER_PROXY to leverage jupyter-server-proxy to +proxy the tile serving port through Jupyter’s authenticated web +interface. This is useful in Docker and cloud JupyterHub +environments. You can set the environment variable +LARGE_IMAGE_JUPYTER_PROXY to control the default value of +JUPYTER_PROXY. If JUPYTER_PROXY is set to True, the +default will be '/proxy/ which will work for most Docker +Jupyter configurations. If in a cloud JupyterHub environment, +this will get a bit more nuanced as the +JUPYTERHUB_SERVICE_PREFIX may need to prefix the +'/proxy/'.

+

To programmatically set these values:

+
from large_image.tilesource.jupyter import IPyLeafletMixin
+
+# Only set one of these values
+
+# Use a custom domain (avoids port proxying)
+IPyLeafletMixin.JUPYTER_HOST = 'mydomain'
+
+# Proxy in a standard JupyterLab environment
+IPyLeafletMixin.JUPYTER_PROXY = True  # defaults to `/proxy/`
+
+# Proxy in a cloud JupyterHub environment
+IPyLeafletMixin.JUPYTER_PROXY = '/jupyter/user/username/proxy/'
+# See if ``JUPYTERHUB_SERVICE_PREFIX`` is in the environment
+# variables to improve this
+
+
+
+
+JUPYTER_HOST = '127.0.0.1'🔗
+
+ +
+
+JUPYTER_PROXY = False🔗
+
+ +
+
+as_leaflet_layer(**kwargs) Any[source]🔗
+
+ +
+
+property iplmap: Any🔗
+

If using ipyleaflets, get access to the map object.

+
+ +
+ +
+
+class large_image.tilesource.jupyter.Map(*, ts: IPyLeafletMixin | None = None, metadata: Dict | None = None, url: str | None = None, gc: Any | None = None, id: str | None = None, resource: str | None = None)[source]🔗
+

Bases: object

+

An IPyLeafletMap representation of a large image.

+

Specify the large image to be used with the IPyLeaflet Map. One of (a) +a tile source, (b) metadata dictionary and tile url, (c) girder client +and item or file id, or (d) girder client and resource path must be +specified.

+
+
Parameters:
+
    +
  • ts – a TileSource.

  • +
  • metadata – a metadata dictionary as returned by a tile source or +a girder item/{id}/tiles endpoint.

  • +
  • url – a slippy map template url to fetch tiles (e.g., +…/item/{id}/tiles/zxy/{z}/{x}/{y}?params=…)

  • +
  • gc – an authenticated girder client.

  • +
  • id – an item id that exists on the girder client.

  • +
  • resource – a girder resource path of an item or file that exists +on the girder client.

  • +
+
+
+
+
+from_map(coordinate: List[float] | Tuple[float, float]) Tuple[float, float][source]🔗
+
+
Parameters:
+

coordinate – a two-tuple that is in the map space coordinates.

+
+
Returns:
+

a two-tuple that is x, y in pixel space or x, y in image +projection space.

+
+
+
+ +
+
+property id: str | None🔗
+
+ +
+
+property layer: Any🔗
+
+ +
+
+make_layer(metadata: Dict, url: str, **kwargs) Any[source]🔗
+

Create an ipyleaflet tile layer given large_image metadata and a tile +url.

+
+ +
+
+make_map(metadata: Dict, layer: Any | None = None, center: Tuple[float, float] | None = None) Any[source]🔗
+

Create an ipyleaflet map given large_image metadata, an optional +ipyleaflet layer, and the center of the tile source.

+
+ +
+
+property map: Any🔗
+
+ +
+
+property metadata: JSONDict🔗
+
+ +
+
+to_map(coordinate: List[float] | Tuple[float, float]) Tuple[float, float][source]🔗
+

Convert a coordinate from the image or projected image space to the map +space.

+
+
Parameters:
+

coordinate – a two-tuple that is x, y in pixel space or x, y in +image projection space.

+
+
Returns:
+

a two-tuple that is in the map space coordinates.

+
+
+
+ +
+
+update_frame(event, **kwargs)[source]🔗
+
+ +
+ +
+
+large_image.tilesource.jupyter.launch_tile_server(tile_source: IPyLeafletMixin, port: int = 0) Any[source]🔗
+
+ +
+
+

large_image.tilesource.resample module🔗

+
+
+class large_image.tilesource.resample.ResampleMethod(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)[source]🔗
+

Bases: Enum

+
+
+NP_MAX = 9🔗
+
+ +
+
+NP_MAX_COLOR = 12🔗
+
+ +
+
+NP_MEAN = 6🔗
+
+ +
+
+NP_MEDIAN = 7🔗
+
+ +
+
+NP_MIN = 10🔗
+
+ +
+
+NP_MIN_COLOR = 13🔗
+
+ +
+
+NP_MODE = 8🔗
+
+ +
+
+NP_NEAREST = 11🔗
+
+ +
+
+PIL_BICUBIC = Resampling.BICUBIC🔗
+
+ +
+
+PIL_BILINEAR = Resampling.BILINEAR🔗
+
+ +
+
+PIL_BOX = Resampling.BOX🔗
+
+ +
+
+PIL_HAMMING = Resampling.HAMMING🔗
+
+ +
+
+PIL_LANCZOS = Resampling.LANCZOS🔗
+
+ +
+
+PIL_MAX_ENUM = Resampling.HAMMING🔗
+
+ +
+
+PIL_NEAREST = Resampling.NEAREST🔗
+
+ +
+ +
+
+large_image.tilesource.resample.downsampleTileHalfRes(tile: ndarray, resample_method: ResampleMethod) ndarray[source]🔗
+
+ +
+
+large_image.tilesource.resample.numpyResize(tile: ndarray, new_shape: Dict, resample_method: ResampleMethod) ndarray[source]🔗
+
+ +
+
+large_image.tilesource.resample.pilResize(tile: ndarray, new_shape: Dict, resample_method: ResampleMethod) ndarray[source]🔗
+
+ +
+
+

large_image.tilesource.stylefuncs module🔗

+
+
+large_image.tilesource.stylefuncs.maskPixelValues(image: ndarray, context: SimpleNamespace, values: List[int | List[int] | Tuple[int, ...]], negative: int | None = None, positive: int | None = None) ndarray[source]🔗
+

This is a style utility function that returns a black-and-white 8-bit image +where the image is white if the pixel of the source image is in a list of +values and black otherwise. The values is a list where each entry can +either be a tuple the same length as the band dimension of the output image +or a single value which is handled as 0xBBGGRR.

+
+
Parameters:
+
    +
  • image – a numpy array of Y, X, Bands.

  • +
  • context – the style context. context.image is the source image

  • +
  • values – an array of values, each of which is either an array of the +same number of bands as the source image or a single value of the form +0xBBGGRR assuming uint8 data.

  • +
  • negative – None to use [0, 0, 0, 255], or an RGBA uint8 value for +pixels not in the value list.

  • +
  • positive – None to use [255, 255, 255, 0], or an RGBA uint8 value for +pixels in the value list.

  • +
+
+
Returns:
+

an RGBA numpy image which is exactly black or transparent white.

+
+
+
+ +
+
+large_image.tilesource.stylefuncs.medianFilter(image: ndarray, context: SimpleNamespace | None = None, kernel: int = 5, weight: float = 1.0) ndarray[source]🔗
+

This is a style utility function that applies a median rank filter to the +image to sharpen it.

+
+
Parameters:
+
    +
  • image – a numpy array of Y, X, Bands.

  • +
  • context – the style context. context.image is the source image

  • +
  • kernel – the filter kernel size.

  • +
  • weight – the weight of the difference between the image and the +filtered image that is used to add into the image. 0 is no effect/

  • +
+
+
Returns:
+

an numpy image which is the filtered version of the source.

+
+
+
+ +
+
+

large_image.tilesource.tiledict module🔗

+
+
+class large_image.tilesource.tiledict.LazyTileDict(tileInfo: Dict[str, Any], *args, **kwargs)[source]🔗
+

Bases: dict

+

Tiles returned from the tile iterator and dictionaries of information with +actual image data in the ‘tile’ key and the format in the ‘format’ key. +Since some applications need information about the tile but don’t need the +image data, these two values are lazily computed. The LazyTileDict can be +treated like a regular dictionary, except that when either of those two +keys are first accessed, they will cause the image to be loaded and +possibly converted to a PIL image and cropped.

+

Unless setFormat is called on the tile, tile images may always be returned +as PIL images.

+

Create a LazyTileDict dictionary where there is enough information to +load the tile image. ang and kwargs are as for the dict() class.

+
+
Parameters:
+

tileInfo – a dictionary of x, y, level, format, encoding, crop, +and source, used for fetching the tile image.

+
+
+
+
+release() None[source]🔗
+

If the tile has been loaded, unload it. It can be loaded again. This +is useful if you want to keep tiles available in memory but not their +actual tile data.

+
+ +
+
+setFormat(format: Tuple[str, ...], resample: bool = False, imageKwargs: Dict[str, Any] | None = None) None[source]🔗
+

Set a more restrictive output format for a tile, possibly also resizing +it via resampling. If this is not called, the tile may either be +returned as one of the specified formats or as a PIL image.

+
+
Parameters:
+
    +
  • format – a tuple or list of allowed formats. Formats are members +of TILE_FORMAT_*. This will avoid converting images if they are +in the desired output encoding (regardless of subparameters).

  • +
  • resample – if not False or None, allow resampling. Once turned +on, this cannot be turned off on the tile.

  • +
  • imageKwargs – additional parameters that should be passed to +_encodeImage.

  • +
+
+
+
+ +
+ +
+
+

large_image.tilesource.tileiterator module🔗

+
+
+class large_image.tilesource.tileiterator.TileIterator(source: tilesource.TileSource, format: str | Tuple[str] = ('numpy',), resample: bool | None = True, **kwargs)[source]🔗
+

Bases: object

+

A tile iterator on a TileSource. Details about the iterator can be read +via the info attribute on the iterator.

+
+ +
+
+

large_image.tilesource.utilities module🔗

+
+
+class large_image.tilesource.utilities.ImageBytes(source: bytes, mimetype: str | None = None)[source]🔗
+

Bases: bytes

+

Wrapper class to make repr of image bytes better in ipython.

+

Display the number of bytes and, if known, the mimetype.

+
+
+property mimetype: str | None🔗
+
+ +
+ +
+
+class large_image.tilesource.utilities.JSONDict(*args, **kwargs)[source]🔗
+

Bases: dict

+

Wrapper class to improve Jupyter repr of JSON-able dicts.

+
+ +
+
+large_image.tilesource.utilities.addPILFormatsToOutputOptions() None[source]🔗
+

Check PIL for available formats that be saved and add them to the lists of +of available formats.

+
+ +
+
+large_image.tilesource.utilities.dictToEtree(d: Dict[str, Any], root: Element | None = None) Element[source]🔗
+

Convert a dictionary in the style produced by etreeToDict back to an etree. +Make an xml string via xml.etree.ElementTree.tostring(dictToEtree( +dictionary), encoding=’utf8’, method=’xml’). Note that this function and +etreeToDict are not perfect conversions; numerical values are quoted in +xml. Plain key-value pairs are ambiguous whether they should be attributes +or text values. Text fields are collected together.

+
+
Parameters:
+

d – a dictionary.

+
+
Prarm root:
+

the root node to attach this dictionary to.

+
+
Returns:
+

an etree.

+
+
+
+ +
+
+large_image.tilesource.utilities.etreeToDict(t: Element) Dict[str, Any][source]🔗
+

Convert an xml etree to a nested dictionary without schema names in the +keys. If you have an xml string, this can be converted to a dictionary via +xml.etree.etreeToDict(ElementTree.fromstring(xml_string)).

+
+
Parameters:
+

t – an etree.

+
+
Returns:
+

a python dictionary with the results.

+
+
+
+ +
+
+large_image.tilesource.utilities.fullAlphaValue(arr: ndarray | dtype[Any] | None | type[Any] | _SupportsDType[dtype[Any]] | str | tuple[Any, int] | tuple[Any, SupportsIndex | Sequence[SupportsIndex]] | list[Any] | _DTypeDict | tuple[Any, Any]) int[source]🔗
+

Given a numpy array, return the value that should be used for a fully +opaque alpha channel. For uint variants, this is the max value.

+
+
Parameters:
+

arr – a numpy array.

+
+
Returns:
+

the value for the alpha channel.

+
+
+
+ +
+
+large_image.tilesource.utilities.getAvailableNamedPalettes(includeColors: bool = True, reduced: bool = False) List[str][source]🔗
+

Get a list of all named palettes that can be used with getPaletteColors.

+
+
Parameters:
+
    +
  • includeColors – if True, include named colors. If False, only +include actual palettes.

  • +
  • reduced – if True, exclude reversed palettes and palettes with +fewer colors where a palette with the same basic name exists with more +colors.

  • +
+
+
Returns:
+

a list of names.

+
+
+
+ +
+
+large_image.tilesource.utilities.getPaletteColors(value: str | List[str | float | Tuple[float, ...]]) ndarray[source]🔗
+

Given a list or a name, return a list of colors in the form of a numpy +array of RGBA. If a list, each entry is a color name resolvable by either +PIL.ImageColor.getcolor, by matplotlib.colors, or a 3 or 4 element list or +tuple of RGB(A) values on a scale of 0-1. If this is NOT a list, then, if +it can be parsed as a color, it is treated as [‘#000’, <value>]. If that +cannot be parsed, then it is assumed to be a named palette in palettable +(such as viridis.Viridis_12) or a named palette in matplotlib (including +plugins).

+
+
Parameters:
+

value – Either a list, a single color name, or a palette name. See +above.

+
+
Returns:
+

a numpy array of RGBA value on the scale of [0-255].

+
+
+
+ +
+
+large_image.tilesource.utilities.getTileFramesQuadInfo(metadata: Dict[str, Any], options: Dict[str, Any] | None = None) Dict[str, Any][source]🔗
+

Compute what tile_frames need to be requested for a particular condition.

+
+
Options is a dictionary of:
+
format:
+

The compression and format for the texture. Defaults to +{‘encoding’: ‘JPEG’, ‘jpegQuality’: 85, ‘jpegSubsampling’: 1}.

+
+
query:
+

Additional query options to add to the tile source, such as +style.

+
+
frameBase:
+

(default 0) Starting frame number used. c/z/xy/z to step +through that index length (0 to 1 less than the value), which is +probably only useful for cache reporting or scheduling.

+
+
frameStride:
+

(default 1) Only use every frameStride frame of the +image. c/z/xy/z to use that axis length.

+
+
frameGroup:
+

(default 1) If above 1 and multiple textures are used, each +texture will have an even multiple of the group size number of +frames. This helps control where texture loading transitions +occur. c/z/xy/z to use that axis length.

+
+
frameGroupFactor:
+

(default 4) If frameGroup would reduce the size +of the tile images beyond this factor, don’t use it.

+
+
frameGroupStride:
+

(default 1) If frameGroup is above 1 and multiple +textures are used, then the frames are reordered based on this +stride value. “auto” to use frameGroup / frameStride if that +value is an integer.

+
+
maxTextureSize:
+

Limit the maximum texture size to a square of this +size.

+
+
maxTextures:
+

(default 1) If more than one, allow multiple textures to +increase the size of the individual frames. The number of textures +will be capped by maxTotalTexturePixels as well as this number.

+
+
maxTotalTexturePixels:
+

(default 1073741824) Limit the maximum texture +size and maximum number of textures so that the combined set does +not exceed this number of pixels.

+
+
alignment:
+

(default 16) Individual frames are buffered to an alignment +of this maxy pixels. If JPEG compression is used, this should +be 8 for monochrome images or jpegs without subsampling, or 16 for +jpegs with moderate subsampling to avoid compression artifacts from +leaking between frames.

+
+
maxFrameSize:
+

If set, limit the maximum width and height of an +individual frame to this value.

+
+
+
+
+
+
Parameters:
+
    +
  • metadata – the tile source metadata. Needs to contain sizeX, sizeY, +tileWidth, tileHeight, and a list of frames.

  • +
  • options – dictionary of options, as described above.

  • +
+
+
Returns:
+

a dictionary of values to use for making calls to tile_frames.

+
+
+
+ +
+
+large_image.tilesource.utilities.histogramThreshold(histogram: Dict[str, Any], threshold: float, fromMax: bool = False) float[source]🔗
+

Given a histogram and a threshold on a scale of [0, 1], return the bin +edge that excludes no more than the specified threshold amount of values. +For instance, a threshold of 0.02 would exclude at most 2% of the values.

+
+
Parameters:
+
    +
  • histogram – a histogram record for a specific channel.

  • +
  • threshold – a value from 0 to 1.

  • +
  • fromMax – if False, return values excluding the low end of the +histogram; if True, return values from excluding the high end of the +histogram.

  • +
+
+
Returns:
+

the value the excludes no more than the threshold from the +specified end.

+
+
+
+ +
+
+large_image.tilesource.utilities.isValidPalette(value: str | List[str | float | Tuple[float, ...]]) bool[source]🔗
+

Check if a value can be used as a palette.

+
+
Parameters:
+

value – Either a list, a single color name, or a palette name. See +getPaletteColors.

+
+
Returns:
+

a boolean; true if the value can be used as a palette.

+
+
+
+ +
+
+large_image.tilesource.utilities.nearPowerOfTwo(val1: float, val2: float, tolerance: float = 0.02) bool[source]🔗
+

Check if two values are different by nearly a power of two.

+
+
Parameters:
+
    +
  • val1 – the first value to check.

  • +
  • val2 – the second value to check.

  • +
  • tolerance – the maximum difference in the log2 ratio’s mantissa.

  • +
+
+
Returns:
+

True if the values are nearly a power of two different from each +other; false otherwise.

+
+
+
+ +
+
+

Module contents🔗

+
+
+class large_image.tilesource.FileTileSource(path: str | Path | Dict[Any, Any], *args, **kwargs)[source]🔗
+

Bases: TileSource

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+classmethod canRead(path: str | Path | Dict[Any, Any], *args, **kwargs) bool[source]🔗
+

Check if we can read the input. This takes the same parameters as +__init__.

+
+
Returns:
+

True if this class can read the input. False if it +cannot.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs) str[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getState() str[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+ +
+
+exception large_image.tilesource.TileGeneralError[source]🔗
+

Bases: Exception

+
+ +
+
+large_image.tilesource.TileGeneralException🔗
+

alias of TileGeneralError

+
+ +
+
+class large_image.tilesource.TileSource(encoding: str = 'JPEG', jpegQuality: int = 95, jpegSubsampling: int = 0, tiffCompression: str = 'raw', edge: bool | str = False, style: str | Dict[str, int] | None = None, noCache: bool | None = None, *args, **kwargs)[source]🔗
+

Bases: IPyLeafletMixin

+

Initialize the tile class.

+
+
Parameters:
+
    +
  • jpegQuality – when serving jpegs, use this quality.

  • +
  • jpegSubsampling – when serving jpegs, use this subsampling (0 is +full chroma, 1 is half, 2 is quarter).

  • +
  • encoding – ‘JPEG’, ‘PNG’, ‘TIFF’, or ‘TILED’.

  • +
  • edge – False to leave edge tiles whole, True or ‘crop’ to crop +edge tiles, otherwise, an #rrggbb color to fill edges.

  • +
  • tiffCompression – the compression format to use when encoding a +TIFF.

  • +
  • style

    if None, use the default style for the file. Otherwise, +this is a string with a json-encoded dictionary. The style can +contain the following keys:

    +
    +
    +
    band:
    +

    if -1 or None, and if style is specified at all, the +greyscale value is used. Otherwise, a 1-based numerical +index into the channels of the image or a string that +matches the interpretation of the band (‘red’, ‘green’, +‘blue’, ‘gray’, ‘alpha’). Note that ‘gray’ on an RGB or +RGBA image will use the green band.

    +
    +
    frame:
    +

    if specified, override the frame value for this band. +When used as part of a bands list, this can be used to +composite multiple frames together. It is most efficient +if at least one band either doesn’t specify a frame +parameter or specifies the same frame value as the primary +query.

    +
    +
    framedelta:
    +

    if specified and frame is not specified, override +the frame value for this band by using the current frame +plus this value.

    +
    +
    min:
    +

    the value to map to the first palette value. Defaults to +0. ‘auto’ to use 0 if the reported minimum and maximum of +the band are between [0, 255] or use the reported minimum +otherwise. ‘min’ or ‘max’ to always uses the reported +minimum or maximum. ‘full’ to always use 0.

    +
    +
    max:
    +

    the value to map to the last palette value. Defaults to +255. ‘auto’ to use 0 if the reported minimum and maximum +of the band are between [0, 255] or use the reported +maximum otherwise. ‘min’ or ‘max’ to always uses the +reported minimum or maximum. ‘full’ to use the maximum +value of the base data type (either 1, 255, or 65535).

    +
    +
    palette:
    +

    a single color string, a palette name, or a list of +two or more color strings. Color strings are of the form +#RRGGBB, #RRGGBBAA, #RGB, #RGBA, or any string parseable by +the PIL modules, or, if it is installed, by matplotlib. A +single color string is the same as the list [‘#000’, +<color>]. Palette names are the name of a palettable +palette or, if available, a matplotlib palette.

    +
    +
    nodata:
    +

    the value to use for missing data. null or unset to +not use a nodata value.

    +
    +
    composite:
    +

    either ‘lighten’ or ‘multiply’. Defaults to +‘lighten’ for all except the alpha band.

    +
    +
    clamp:
    +

    either True to clamp (also called clip or crop) values +outside of the [min, max] to the ends of the palette or +False to make outside values transparent.

    +
    +
    dtype:
    +

    convert the results to the specified numpy dtype. +Normally, if a style is applied, the results are +intermediately a float numpy array with a value range of +[0,255]. If this is ‘uint16’, it will be cast to that and +multiplied by 65535/255. If ‘float’, it will be divided by +255. If ‘source’, this uses the dtype of the source image.

    +
    +
    axis:
    +

    keep only the specified axis from the numpy intermediate +results. This can be used to extract a single channel +after compositing.

    +
    +
    +
    +

    Alternately, the style object can contain a single key of ‘bands’, +which has a value which is a list of style dictionaries as above, +excepting that each must have a band that is not -1. Bands are +composited in the order listed. This base object may also contain +the ‘dtype’ and ‘axis’ values.

    +

  • +
  • noCache – if True, the style can be adjusted dynamically and the +source is not elibible for caching. If there is no intention to +reuse the source at a later time, this can have performance +benefits, such as when first cataloging images that can be read.

  • +
+
+
+
+
+property bandCount: int | None🔗
+
+ +
+
+classmethod canRead(*args, **kwargs)[source]🔗
+

Check if we can read the input. This takes the same parameters as +__init__.

+
+
Returns:
+

True if this class can read the input. False if it cannot.

+
+
+
+ +
+
+convertRegionScale(sourceRegion: Dict[str, Any], sourceScale: Dict[str, float] | None = None, targetScale: Dict[str, float] | None = None, targetUnits: str | None = None, cropToImage: bool = True) Dict[str, Any][source]🔗
+

Convert a region from one scale to another.

+
+
Parameters:
+
    +
  • sourceRegion

    a dictionary of optional values which specify the +part of an image to process.

    +
    +
    left:
    +

    the left edge (inclusive) of the region to process.

    +
    +
    top:
    +

    the top edge (inclusive) of the region to process.

    +
    +
    right:
    +

    the right edge (exclusive) of the region to process.

    +
    +
    bottom:
    +

    the bottom edge (exclusive) of the region to process.

    +
    +
    width:
    +

    the width of the region to process.

    +
    +
    height:
    +

    the height of the region to process.

    +
    +
    units:
    +

    either ‘base_pixels’ (default), ‘pixels’, ‘mm’, or +‘fraction’. base_pixels are in maximum resolution pixels. +pixels is in the specified magnification pixels. mm is in the +specified magnification scale. fraction is a scale of 0 to 1. +pixels and mm are only available if the magnification and mm +per pixel are defined for the image.

    +
    +
    +

  • +
  • sourceScale

    a dictionary of optional values which specify the +scale of the source region. Required if the sourceRegion is +in “mag_pixels” units.

    +
    +
    magnification:
    +

    the magnification ratio.

    +
    +
    mm_x:
    +

    the horizontal size of a pixel in millimeters.

    +
    +
    mm_y:
    +

    the vertical size of a pixel in millimeters.

    +
    +
    +

  • +
  • targetScale

    a dictionary of optional values which specify the +scale of the target region. Required in targetUnits is in +“mag_pixels” units.

    +
    +
    magnification:
    +

    the magnification ratio.

    +
    +
    mm_x:
    +

    the horizontal size of a pixel in millimeters.

    +
    +
    mm_y:
    +

    the vertical size of a pixel in millimeters.

    +
    +
    +

  • +
  • targetUnits – if not None, convert the region to these units. +Otherwise, the units are will either be the sourceRegion units if +those are not “mag_pixels” or base_pixels. If “mag_pixels”, the +targetScale must be specified.

  • +
  • cropToImage – if True, don’t return region coordinates outside of +the image.

  • +
+
+
+
+ +
+
+property dtype: dtype🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {None: SourcePriority.FALLBACK}🔗
+
+ +
+
+property frames: int🔗
+

A property with the number of frames.

+
+ +
+
+property geospatial: bool🔗
+
+ +
+
+getAssociatedImage(imageKey: str, *args, **kwargs) Tuple[ImageBytes, str] | None[source]🔗
+

Return an associated image.

+
+
Parameters:
+
    +
  • imageKey – the key of the associated image to retrieve.

  • +
  • kwargs – optional arguments. Some options are width, height, +encoding, jpegQuality, jpegSubsampling, and tiffCompression.

  • +
+
+
Returns:
+

imageData, imageMime: the image data and the mime type, or +None if the associated image doesn’t exist.

+
+
+
+ +
+
+getAssociatedImagesList() List[str][source]🔗
+

Return a list of associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getBandInformation(statistics: bool = False, **kwargs) Dict[int, Any][source]🔗
+

Get information about each band in the image.

+
+
Parameters:
+

statistics – if True, compute statistics if they don’t already +exist.

+
+
Returns:
+

a dictionary of one dictionary per band. Each dictionary +contains known values such as interpretation, min, max, mean, +stdev.

+
+
+
+ +
+
+getBounds(*args, **kwargs) Dict[str, Any][source]🔗
+
+ +
+
+getCenter(*args, **kwargs) Tuple[float, float][source]🔗
+

Returns (Y, X) center location.

+
+ +
+
+getICCProfiles(idx: int | None = None, onlyInfo: bool = False) ImageCmsProfile | List[ImageCmsProfile | None] | None[source]🔗
+

Get a list of all ICC profiles that are available for the source, or +get a specific profile.

+
+
Parameters:
+
    +
  • idx – a 0-based index into the profiles to get one profile, or +None to get a list of all profiles.

  • +
  • onlyInfo – if idx is None and this is true, just return the +profile information.

  • +
+
+
Returns:
+

either one or a list of PIL.ImageCms.CmsProfile objects, or +None if no profiles are available. If a list, entries in the list +may be None.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs) str[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getLevelForMagnification(magnification: float | None = None, exact: bool = False, mm_x: float | None = None, mm_y: float | None = None, rounding: str | bool | None = 'round', **kwargs) int | float | None[source]🔗
+

Get the level for a specific magnification or pixel size. If the +magnification is unknown or no level is sufficient resolution, and an +exact match is not requested, the highest level will be returned.

+

If none of magnification, mm_x, and mm_y are specified, the maximum +level is returned. If more than one of these values is given, an +average of those given will be used (exact will require all of them to +match).

+
+
Parameters:
+
    +
  • magnification – the magnification ratio.

  • +
  • exact – if True, only a level that matches exactly will be +returned.

  • +
  • mm_x – the horizontal size of a pixel in millimeters.

  • +
  • mm_y – the vertical size of a pixel in millimeters.

  • +
  • rounding – if False, a fractional level may be returned. If +‘ceil’ or ‘round’, that function is used to convert the level to an +integer (the exact flag still applies). If None, the level is not +cropped to the actual image’s level range.

  • +
+
+
Returns:
+

the selected level or None for no match.

+
+
+
+ +
+
+getMagnificationForLevel(level: float | None = None) Dict[str, float | None][source]🔗
+

Get the magnification at a particular level.

+
+
Parameters:
+

level – None to use the maximum level, otherwise the level to get +the magnification factor of.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getMetadata() JSONDict[source]🔗
+

Return metadata about this tile source. This contains

+
+
+
levels:
+

number of tile levels in this image.

+
+
sizeX:
+

width of the image in pixels.

+
+
sizeY:
+

height of the image in pixels.

+
+
tileWidth:
+

width of a tile in pixels.

+
+
tileHeight:
+

height of a tile in pixels.

+
+
magnification:
+

if known, the magnificaiton of the image.

+
+
mm_x:
+

if known, the width of a pixel in millimeters.

+
+
mm_y:
+

if known, the height of a pixel in millimeters.

+
+
dtype:
+

if known, the type of values in this image.

+
+
+

In addition to the keys that listed above, tile sources that expose +multiple frames will also contain

+
+
frames:
+

a list of frames. Each frame entry is a dictionary with

+
+
Frame:
+

a 0-values frame index (the location in the list)

+
+
Channel:
+

optional. The name of the channel, if known

+
+
IndexC:
+

optional if unique. A 0-based index into the channel +list

+
+
IndexT:
+

optional if unique. A 0-based index for time values

+
+
IndexZ:
+

optional if unique. A 0-based index for z values

+
+
IndexXY:
+

optional if unique. A 0-based index for view (xy) +values

+
+
Index<axis>:
+

optional if unique. A 0-based index for an +arbitrary axis.

+
+
Index:
+

a 0-based index of non-channel unique sets. If the +frames vary only by channel and are adjacent, they will +have the same index.

+
+
+
+
IndexRange:
+

a dictionary of the number of unique index values from +frames if greater than 1 (e.g., if an entry like IndexXY is not +present, then all frames either do not have that value or have +a value of 0).

+
+
IndexStride:
+

a dictionary of the spacing between frames where +unique axes values change.

+
+
channels:
+

optional. If known, a list of channel names

+
+
channelmap:
+

optional. If known, a dictionary of channel names +with their offset into the channel list.

+
+
+
+

Note that this does not include band information, though some tile +sources may do so.

+
+ +
+
+getNativeMagnification() Dict[str, float | None][source]🔗
+

Get the magnification for the highest-resolution level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getOneBandInformation(band: int) Dict[str, Any][source]🔗
+

Get band information for a single band.

+
+
Parameters:
+

band – a 1-based band.

+
+
Returns:
+

a dictionary of band information. See getBandInformation.

+
+
+
+ +
+
+getPixel(includeTileRecord: bool = False, **kwargs) JSONDict[source]🔗
+

Get a single pixel from the current tile source.

+
+
Parameters:
+
    +
  • includeTileRecord – if True, include the tile used for computing +the pixel in the response.

  • +
  • kwargs – optional arguments. Some options are region, output, +encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See +tileIterator.

  • +
+
+
Returns:
+

a dictionary with the value of the pixel for each channel on +a scale of [0-255], including alpha, if available. This may +contain additional information.

+
+
+
+ +
+
+getPointAtAnotherScale(point: Tuple[float, float], sourceScale: Dict[str, float] | None = None, sourceUnits: str | None = None, targetScale: Dict[str, float] | None = None, targetUnits: str | None = None, **kwargs) Tuple[float, float][source]🔗
+

Given a point as a (x, y) tuple, convert it from one scale to another. +The sourceScale, sourceUnits, targetScale, and targetUnits parameters +are the same as convertRegionScale, where sourceUnits are the units +used with sourceScale.

+
+ +
+
+getPreferredLevel(level: int) int[source]🔗
+

Given a desired level (0 is minimum resolution, self.levels - 1 is max +resolution), return the level that contains actual data that is no +lower resolution.

+
+
Parameters:
+

level – desired level

+
+
Returns level:
+

a level with actual data that is no lower resolution.

+
+
+
+ +
+
+getRegion(format: str | Tuple[str] = ('image',), **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

Get a rectangular region from the current tile source. Aspect ratio is +preserved. If neither width nor height is given, the original size of +the highest resolution level is used. If both are given, the returned +image will be no larger than either size.

+
+
Parameters:
+
    +
  • format – the desired format or a tuple of allowed formats. +Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, +TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding may be +specified.

  • +
  • kwargs – optional arguments. Some options are region, output, +encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See +tileIterator.

  • +
+
+
Returns:
+

regionData, formatOrRegionMime: the image data and either the +mime type, if the format is TILE_FORMAT_IMAGE, or the format.

+
+
+
+ +
+
+getRegionAtAnotherScale(sourceRegion: Dict[str, Any], sourceScale: Dict[str, float] | None = None, targetScale: Dict[str, float] | None = None, targetUnits: str | None = None, **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

This takes the same parameters and returns the same results as +getRegion, except instead of region and scale, it takes sourceRegion, +sourceScale, targetScale, and targetUnits. These parameters are the +same as convertRegionScale. See those two functions for parameter +definitions.

+
+ +
+
+getSingleTile(*args, **kwargs) LazyTileDict | None[source]🔗
+

Return any single tile from an iterator. This takes exactly the same +parameters as tileIterator. Use tile_position to get a specific tile, +otherwise the first tile is returned.

+
+
Returns:
+

a tile dictionary or None.

+
+
+
+ +
+
+getSingleTileAtAnotherScale(*args, **kwargs) LazyTileDict | None[source]🔗
+

Return any single tile from a rescaled iterator. This takes exactly +the same parameters as tileIteratorAtAnotherScale. Use tile_position +to get a specific tile, otherwise the first tile is returned.

+
+
Returns:
+

a tile dictionary or None.

+
+
+
+ +
+
+getState() str[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getThumbnail(width: str | int | None = None, height: str | int | None = None, **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

Get a basic thumbnail from the current tile source. Aspect ratio is +preserved. If neither width nor height is given, a default value is +used. If both are given, the thumbnail will be no larger than either +size. A thumbnail has the same options as a region except that it +always includes the entire image and has a default size of 256 x 256.

+
+
Parameters:
+
    +
  • width – maximum width in pixels.

  • +
  • height – maximum height in pixels.

  • +
  • kwargs – optional arguments. Some options are encoding, +jpegQuality, jpegSubsampling, and tiffCompression.

  • +
+
+
Returns:
+

thumbData, thumbMime: the image data and the mime type.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, sparseFallback=False, frame=None)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+getTileCount(*args, **kwargs) int[source]🔗
+

Return the number of tiles that the tileIterator will return. See +tileIterator for parameters.

+
+
Returns:
+

the number of tiles that the tileIterator will yield.

+
+
+
+ +
+
+getTileMimeType() str[source]🔗
+

Return the default mimetype for image tiles.

+
+
Returns:
+

the mime type of the tile.

+
+
+
+ +
+
+histogram(dtype: dtype[Any] | None | type[Any] | _SupportsDType[dtype[Any]] | str | tuple[Any, int] | tuple[Any, SupportsIndex | Sequence[SupportsIndex]] | list[Any] | _DTypeDict | tuple[Any, Any] = None, onlyMinMax: bool = False, bins: int = 256, density: bool = False, format: Any = None, *args, **kwargs) Dict[str, ndarray | List[Dict[str, Any]]][source]🔗
+

Get a histogram for a region.

+
+
Parameters:
+
    +
  • dtype – if specified, the tiles must be this numpy.dtype.

  • +
  • onlyMinMax – if True, only return the minimum and maximum value +of the region.

  • +
  • bins – the number of bins in the histogram. This is passed to +numpy.histogram, but needs to produce the same set of edges for +each tile.

  • +
  • density – if True, scale the results based on the number of +samples.

  • +
  • format – ignored. Used to override the format for the +tileIterator.

  • +
  • range – if None, use the computed min and (max + 1). Otherwise, +this is the range passed to numpy.histogram. Note this is only +accessible via kwargs as it otherwise overloads the range function. +If ‘round’, use the computed values, but the number of bins may be +reduced or the bin_edges rounded to integer values for +integer-based source data.

  • +
  • args – parameters to pass to the tileIterator.

  • +
  • kwargs – parameters to pass to the tileIterator.

  • +
+
+
Returns:
+

if onlyMinMax is true, this is a dictionary with keys min and +max, each of which is a numpy array with the minimum and maximum of +all of the bands. If onlyMinMax is False, this is a dictionary +with a single key ‘histogram’ that contains a list of histograms +per band. Each entry is a dictionary with min, max, range, hist, +bins, and bin_edges. range is [min, (max + 1)]. hist is the +counts (normalized if density is True) for each bin. bins is the +number of bins used. bin_edges is an array one longer than the +hist array that contains the boundaries between bins.

+
+
+
+ +
+
+levels: int🔗
+
+ +
+
+property metadata: JSONDict🔗
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = None🔗
+
+ +
+
+nameMatches: Dict[str, SourcePriority] = {}🔗
+
+ +
+
+newPriority: SourcePriority | None = None🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+property style🔗
+
+ +
+
+tileFrames(format: str | Tuple[str] = ('image',), frameList: List[int] | None = None, framesAcross: int | None = None, max_workers: int | None = -4, **kwargs) Tuple[ndarray | Image | ImageBytes | bytes | Path, str][source]🔗
+

Given the parameters for getRegion, plus a list of frames and the +number of frames across, make a larger image composed of a region from +each listed frame composited together.

+
+
Parameters:
+
    +
  • format – the desired format or a tuple of allowed formats. +Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, +TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding may be +specified.

  • +
  • frameList – None for all frames, or a list of 0-based integers.

  • +
  • framesAcross – the number of frames across the final image. If +unspecified, this is the ceiling of sqrt(number of frames in frame +list).

  • +
  • kwargs – optional arguments. Some options are region, output, +encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See +tileIterator.

  • +
  • max_workers – maximum workers for parallelism. If negative, use +the minimum of the absolute value of this number or +multiprocessing.cpu_count().

  • +
+
+
Returns:
+

regionData, formatOrRegionMime: the image data and either the +mime type, if the format is TILE_FORMAT_IMAGE, or the format.

+
+
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileIterator(format: str | Tuple[str] = ('numpy',), resample: bool = True, **kwargs) Iterator[LazyTileDict][source]🔗
+

Iterate on all tiles in the specified region at the specified scale. +Each tile is returned as part of a dictionary that includes

+
+
+
x, y:
+

(left, top) coordinates in current magnification pixels

+
+
width, height:
+

size of current tile in current magnification pixels

+
+
tile:
+

cropped tile image

+
+
format:
+

format of the tile

+
+
level:
+

level of the current tile

+
+
level_x, level_y:
+

the tile reference number within the level. +Tiles are numbered (0, 0), (1, 0), (2, 0), etc. The 0th tile +yielded may not be (0, 0) if a region is specified.

+
+
tile_position:
+

a dictionary of the tile position within the +iterator, containing:

+
+
level_x, level_y:
+

the tile reference number within the level.

+
+
region_x, region_y:
+

0, 0 is the first tile in the full +iteration (when not restricting the iteration to a single +tile).

+
+
position:
+

a 0-based value for the tile within the full +iteration.

+
+
+
+
iterator_range:
+

a dictionary of the output range of the iterator:

+
+
level_x_min, level_x_max:
+

the tiles that are be included +during the full iteration: [layer_x_min, layer_x_max).

+
+
level_y_min, level_y_max:
+

the tiles that are be included +during the full iteration: [layer_y_min, layer_y_max).

+
+
region_x_max, region_y_max:
+

the number of tiles included during +the full iteration. This is layer_x_max - layer_x_min, +layer_y_max - layer_y_min.

+
+
position:
+

the total number of tiles included in the full +iteration. This is region_x_max * region_y_max.

+
+
+
+
magnification:
+

magnification of the current tile

+
+
mm_x, mm_y:
+

size of the current tile pixel in millimeters.

+
+
gx, gy:
+

(left, top) coordinates in maximum-resolution pixels

+
+
gwidth, gheight:
+

size of of the current tile in maximum-resolution +pixels.

+
+
tile_overlap:
+

the amount of overlap with neighboring tiles (left, +top, right, and bottom). Overlap never extends outside of the +requested region.

+
+
+
+

If a region that includes partial tiles is requested, those tiles are +cropped appropriately. Most images will have tiles that get cropped +along the right and bottom edges in any case. If an exact +magnification or scale is requested, no tiles will be returned.

+
+
Parameters:
+
    +
  • format – the desired format or a tuple of allowed formats. +Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, +TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding must be +specified.

  • +
  • resample

    If True or one of PIL.Image.Resampling.NEAREST, +LANCZOS, BILINEAR, or BICUBIC to resample tiles that are not the +target output size. Tiles that are resampled will have additional +dictionary entries of:

    +
    +
    scaled:
    +

    the scaling factor that was applied (less than 1 is +downsampled).

    +
    +
    tile_x, tile_y:
    +

    (left, top) coordinates before scaling

    +
    +
    tile_width, tile_height:
    +

    size of the current tile before +scaling.

    +
    +
    tile_magnification:
    +

    magnification of the current tile before +scaling.

    +
    +
    tile_mm_x, tile_mm_y:
    +

    size of a pixel in a tile in millimeters +before scaling.

    +
    +
    +

    Note that scipy.misc.imresize uses PIL internally.

    +

  • +
  • region

    a dictionary of optional values which specify the part +of the image to process:

    +
    +
    left:
    +

    the left edge (inclusive) of the region to process.

    +
    +
    top:
    +

    the top edge (inclusive) of the region to process.

    +
    +
    right:
    +

    the right edge (exclusive) of the region to process.

    +
    +
    bottom:
    +

    the bottom edge (exclusive) of the region to process.

    +
    +
    width:
    +

    the width of the region to process.

    +
    +
    height:
    +

    the height of the region to process.

    +
    +
    units:
    +

    either ‘base_pixels’ (default), ‘pixels’, ‘mm’, or +‘fraction’. base_pixels are in maximum resolution pixels. +pixels is in the specified magnification pixels. mm is in the +specified magnification scale. fraction is a scale of 0 to 1. +pixels and mm are only available if the magnification and mm +per pixel are defined for the image.

    +
    +
    +

  • +
  • output

    a dictionary of optional values which specify the size +of the output.

    +
    +
    maxWidth:
    +

    maximum width in pixels. If either maxWidth or maxHeight +is specified, magnification, mm_x, and mm_y are ignored.

    +
    +
    maxHeight:
    +

    maximum height in pixels.

    +
    +
    +

  • +
  • scale

    a dictionary of optional values which specify the scale +of the region and / or output. This applies to region if +pixels or mm are used for inits. It applies to output if +neither output maxWidth nor maxHeight is specified.

    +
    +
    magnification:
    +

    the magnification ratio. Only used if maxWidth and +maxHeight are not specified or None.

    +
    +
    mm_x:
    +

    the horizontal size of a pixel in millimeters.

    +
    +
    mm_y:
    +

    the vertical size of a pixel in millimeters.

    +
    +
    exact:
    +

    if True, only a level that matches exactly will be returned. +This is only applied if magnification, mm_x, or mm_y is used.

    +
    +
    +

  • +
  • tile_position – if present, either a number to only yield the +(tile_position)th tile [0 to (xmax - min) * (ymax - ymin)) that the +iterator would yield, or a dictionary of {region_x, region_y} to +yield that tile, where 0, 0 is the first tile yielded, and +xmax - xmin - 1, ymax - ymin - 1 is the last tile yielded, or a +dictionary of {level_x, level_y} to yield that specific tile if it +is in the region.

  • +
  • tile_size

    if present, retile the output to the specified tile +size. If only width or only height is specified, the resultant +tiles will be square. This is a dictionary containing at least +one of:

    +
    +
    width:
    +

    the desired tile width.

    +
    +
    height:
    +

    the desired tile height.

    +
    +
    +

  • +
  • tile_overlap

    if present, retile the output adding a symmetric +overlap to the tiles. If either x or y is not specified, it +defaults to zero. The overlap does not change the tile size, +only the stride of the tiles. This is a dictionary containing:

    +
    +
    x:
    +

    the horizontal overlap in pixels.

    +
    +
    y:
    +

    the vertical overlap in pixels.

    +
    +
    edges:
    +

    if True, then the edge tiles will exclude the overlap +distance. If unset or False, the edge tiles are full size.

    +

    The overlap is conceptually split between the two sides of +the tile. This is only relevant to where overlap is reported +or if edges is True

    +

    As an example, suppose an image that is 8 pixels across +(01234567) and a tile size of 5 is requested with an overlap of +4. If the edges option is False (the default), the following +tiles are returned: 01234, 12345, 23456, 34567. Each tile +reports its overlap, and the non-overlapped area of each tile +is 012, 3, 4, 567. If the edges option is True, the tiles +returned are: 012, 0123, 01234, 12345, 23456, 34567, 4567, 567, +with the non-overlapped area of each as 0, 1, 2, 3, 4, 5, 6, 7.

    +
    +
    +

  • +
  • tile_offset

    if present, adjust tile positions so that the +corner of one tile is at the specified location.

    +
    +
    left:
    +

    the left offset in pixels.

    +
    +
    top:
    +

    the top offset in pixels.

    +
    +
    auto:
    +

    a boolean, if True, automatically set the offset to align +with the region’s left and top.

    +
    +
    +

  • +
  • encoding – if format includes TILE_FORMAT_IMAGE, a valid PIL +encoding (typically ‘PNG’, ‘JPEG’, or ‘TIFF’) or ‘TILED’ (identical +to TIFF). Must also be in the TileOutputMimeTypes map.

  • +
  • jpegQuality – the quality to use when encoding a JPEG.

  • +
  • jpegSubsampling – the subsampling level to use when encoding a +JPEG.

  • +
  • tiffCompression – the compression format when encoding a TIFF. +This is usually ‘raw’, ‘tiff_lzw’, ‘jpeg’, or ‘tiff_adobe_deflate’. +Some of these are aliased: ‘none’, ‘lzw’, ‘deflate’.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
  • kwargs – optional arguments.

  • +
+
+
Yields:
+

an iterator that returns a dictionary as listed above.

+
+
+
+ +
+
+tileIteratorAtAnotherScale(sourceRegion: Dict[str, Any], sourceScale: Dict[str, float] | None = None, targetScale: Dict[str, float] | None = None, targetUnits: str | None = None, **kwargs) Iterator[LazyTileDict][source]🔗
+

This takes the same parameters and returns the same results as +tileIterator, except instead of region and scale, it takes +sourceRegion, sourceScale, targetScale, and targetUnits. These +parameters are the same as convertRegionScale. See those two functions +for parameter definitions.

+
+ +
+
+tileWidth: int🔗
+
+ +
+
+wrapKey(*args, **kwargs) str[source]🔗
+

Return a key for a tile source and function parameters that can be used +as a unique cache key.

+
+
Parameters:
+
    +
  • args – arguments to add to the hash.

  • +
  • kwaths – arguments to add to the hash.

  • +
+
+
Returns:
+

a cache key.

+
+
+
+ +
+ +
+
+exception large_image.tilesource.TileSourceAssetstoreError[source]🔗
+

Bases: TileSourceError

+
+ +
+
+large_image.tilesource.TileSourceAssetstoreException🔗
+

alias of TileSourceAssetstoreError

+
+ +
+
+exception large_image.tilesource.TileSourceError[source]🔗
+

Bases: TileGeneralError

+
+ +
+
+large_image.tilesource.TileSourceException🔗
+

alias of TileSourceError

+
+ +
+
+exception large_image.tilesource.TileSourceFileNotFoundError(*args, **kwargs)[source]🔗
+

Bases: TileSourceError, FileNotFoundError

+
+ +
+
+large_image.tilesource.canRead(*args, **kwargs) bool[source]🔗
+

Check if large_image can read a path or uri.

+

If there is no intention to open the image immediately, conisder adding +noCache=True to the kwargs to avoid cycling the cache unnecessarily.

+
+
Returns:
+

True if any appropriate source reports it can read the path or +uri.

+
+
+
+ +
+
+large_image.tilesource.dictToEtree(d: Dict[str, Any], root: Element | None = None) Element[source]🔗
+

Convert a dictionary in the style produced by etreeToDict back to an etree. +Make an xml string via xml.etree.ElementTree.tostring(dictToEtree( +dictionary), encoding=’utf8’, method=’xml’). Note that this function and +etreeToDict are not perfect conversions; numerical values are quoted in +xml. Plain key-value pairs are ambiguous whether they should be attributes +or text values. Text fields are collected together.

+
+
Parameters:
+

d – a dictionary.

+
+
Prarm root:
+

the root node to attach this dictionary to.

+
+
Returns:
+

an etree.

+
+
+
+ +
+
+large_image.tilesource.etreeToDict(t: Element) Dict[str, Any][source]🔗
+

Convert an xml etree to a nested dictionary without schema names in the +keys. If you have an xml string, this can be converted to a dictionary via +xml.etree.etreeToDict(ElementTree.fromstring(xml_string)).

+
+
Parameters:
+

t – an etree.

+
+
Returns:
+

a python dictionary with the results.

+
+
+
+ +
+
+large_image.tilesource.getSourceNameFromDict(availableSources: Dict[str, Type[FileTileSource]], pathOrUri: str | PosixPath, mimeType: str | None = None, *args, **kwargs) str | None[source]🔗
+

Get a tile source based on a ordered dictionary of known sources and a path +name or URI. Additional parameters are passed to the tile source and can +be used for properties such as encoding.

+
+
Parameters:
+
    +
  • availableSources – an ordered dictionary of sources to try.

  • +
  • pathOrUri – either a file path or a fixed source via +large_image://<source>.

  • +
  • mimeType – the mimetype of the file, if known.

  • +
+
+
Returns:
+

the name of a tile source that can read the input, or None if +there is no such source.

+
+
+
+ +
+
+large_image.tilesource.getTileSource(*args, **kwargs) FileTileSource[source]🔗
+

Get a tilesource using the known sources. If tile sources have not yet +been loaded, load them.

+
+
Returns:
+

A tilesource for the passed arguments.

+
+
+
+ +
+
+large_image.tilesource.listExtensions(availableSources: Dict[str, Type[FileTileSource]] | None = None) List[str][source]🔗
+

Get a list of all known extensions.

+
+
Parameters:
+

availableSources – an ordered dictionary of sources to try.

+
+
Returns:
+

a list of extensions (without leading dots).

+
+
+
+ +
+
+large_image.tilesource.listMimeTypes(availableSources: Dict[str, Type[FileTileSource]] | None = None) List[str][source]🔗
+

Get a list of all known mime types.

+
+
Parameters:
+

availableSources – an ordered dictionary of sources to try.

+
+
Returns:
+

a list of mime types.

+
+
+
+ +
+
+large_image.tilesource.listSources(availableSources: Dict[str, Type[FileTileSource]] | None = None) Dict[str, Dict[str, Any]][source]🔗
+

Get a dictionary with all sources, all known extensions, and all known +mimetypes.

+
+
Parameters:
+

availableSources – an ordered dictionary of sources to try.

+
+
Returns:
+

a dictionary with sources, extensions, and mimeTypes. The +extensions and mimeTypes list their matching sources in priority order. +The sources list their supported extensions and mimeTypes with their +priority.

+
+
+
+ +
+
+large_image.tilesource.nearPowerOfTwo(val1: float, val2: float, tolerance: float = 0.02) bool[source]🔗
+

Check if two values are different by nearly a power of two.

+
+
Parameters:
+
    +
  • val1 – the first value to check.

  • +
  • val2 – the second value to check.

  • +
  • tolerance – the maximum difference in the log2 ratio’s mantissa.

  • +
+
+
Returns:
+

True if the values are nearly a power of two different from each +other; false otherwise.

+
+
+
+ +
+
+large_image.tilesource.new(*args, **kwargs) TileSource[source]🔗
+

Create a new image.

+

TODO: add specific arguments to choose a source based on criteria.

+
+ +
+
+large_image.tilesource.open(*args, **kwargs) FileTileSource[source]🔗
+

Alternate name of getTileSource.

+

Get a tilesource using the known sources. If tile sources have not yet +been loaded, load them.

+
+
Returns:
+

A tilesource for the passed arguments.

+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image/modules.html b/_build/large_image/modules.html new file mode 100644 index 000000000..ddfdd5bbf --- /dev/null +++ b/_build/large_image/modules.html @@ -0,0 +1,227 @@ + + + + + + + large_image — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image🔗

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_converter/large_image_converter.html b/_build/large_image_converter/large_image_converter.html new file mode 100644 index 000000000..f2ed13a4a --- /dev/null +++ b/_build/large_image_converter/large_image_converter.html @@ -0,0 +1,389 @@ + + + + + + + large_image_converter package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_converter package🔗

+
+

Submodules🔗

+
+
+

large_image_converter.format_aperio module🔗

+
+
+large_image_converter.format_aperio.adjust_params(geospatial, params, **kwargs)[source]🔗
+

Adjust options for aperio format.

+
+
Parameters:
+
    +
  • geospatial – True if the source is geospatial.

  • +
  • params – the conversion options. Possibly modified.

  • +
+
+
Returns:
+

suffix: the recommended suffix for the new file.

+
+
+
+ +
+
+large_image_converter.format_aperio.create_thumbnail_and_label(tempPath, info, ifdCount, needsLabel, labelPosition, **kwargs)[source]🔗
+

Create a thumbnail and, optionally, label image for the aperio file.

+
+
Parameters:
+
    +
  • tempPath – a temporary file in a temporary directory.

  • +
  • info – the tifftools info that will be written to the tiff tile; +modified.

  • +
  • ifdCount – the number of ifds in the first tiled image. This is 1 if +there are subifds.

  • +
  • needsLabel – true if a label image needs to be added.

  • +
  • labelPosition – the position in the ifd list where a label image +should be inserted.

  • +
+
+
+
+ +
+
+large_image_converter.format_aperio.modify_tiff_before_write(info, ifdIndices, tempPath, lidata, **kwargs)[source]🔗
+

Adjust the metadata and ifds for a tiff file to make it compatible with +Aperio (svs).

+

Aperio files are tiff files which are stored without subifds in the order +full res, optional thumbnail, half res, quarter res, …, full res, half +res, quarter res, …, label, macro. All ifds have an ImageDescription +that start with an aperio header followed by some dimension information and +then an option key-.value list

+
+
Parameters:
+
    +
  • info – the tifftools info that will be written to the tiff tile; +modified.

  • +
  • ifdIndices – the 0-based index of the full resolution ifd of each +frame followed by the ifd of the first associated image.

  • +
  • tempPath – a temporary file in a temporary directory.

  • +
  • lidata – large_image data including metadata and associated images.

  • +
+
+
+
+ +
+
+large_image_converter.format_aperio.modify_tiled_ifd(info, ifd, idx, ifdIndices, lidata, liDesc, **kwargs)[source]🔗
+

Modify a tiled image to add aperio metadata and ensure tags are set +appropriately.

+
+
Parameters:
+
    +
  • info – the tifftools info that will be written to the tiff tile; +modified.

  • +
  • ifd – the full resolution ifd as read by tifftools.

  • +
  • idx – index of this ifd.

  • +
  • ifdIndices – the 0-based index of the full resolution ifd of each +frame followed by the ifd of the first associated image.

  • +
  • lidata – large_image data including metadata and associated images.

  • +
  • liDesc – the parsed json from the original large_image_converter +description.

  • +
+
+
+
+ +
+
+large_image_converter.format_aperio.modify_vips_image_before_output(image, convertParams, **kwargs)[source]🔗
+

Make sure the vips image is either 1 or 3 bands.

+
+
Parameters:
+
    +
  • image – a vips image.

  • +
  • convertParams – the parameters that will be used for compression.

  • +
+
+
Returns:
+

a vips image.

+
+
+
+ +
+
+

Module contents🔗

+
+
+large_image_converter.convert(inputPath, outputPath=None, **kwargs)[source]🔗
+

Take a source input file and output a pyramidal tiff file.

+
+
Parameters:
+
    +
  • inputPath – the path to the input file or base file of a set.

  • +
  • outputPath – the path of the output file.

  • +
+
+
+

Optional parameters that can be specified in kwargs:

+
+
Parameters:
+
    +
  • tileSize – the horizontal and vertical tile size.

  • +
  • format – one of ‘tiff’ or ‘aperio’. Default is ‘tiff’.

  • +
  • onlyFrame – None for all frames or the 0-based frame number to just +convert a single frame of the source.

  • +
  • compression – one of ‘jpeg’, ‘deflate’ (zip), ‘lzw’, ‘packbits’, +‘zstd’, or ‘none’.

  • +
  • quality – a jpeg or webp quality passed to vips. 0 is small, 100 is +high quality. 90 or above is recommended. For webp, 0 is lossless.

  • +
  • level – compression level for zstd, 1-22 (default is 10) and deflate, +1-9.

  • +
  • predictor – one of ‘none’, ‘horizontal’, ‘float’, or ‘yes’ used for +lzw and deflate. Default is horizontal for non-geospatial data and yes +for geospatial.

  • +
  • psnr – psnr value for jp2k, higher results in large files. 0 is +lossless.

  • +
  • cr – jp2k compression ratio. 1 is lossless, 100 will try to make +a file 1% the size of the original, etc.

  • +
  • subifds – if True (the default), when creating a multi-frame file, +store lower resolution tiles in sub-ifds. If False, store all data in +primary ifds.

  • +
  • overwrite – if not True, throw an exception if the output path +already exists.

  • +
+
+
+

Additional optional parameters:

+
+
Parameters:
+
    +
  • geospatial – if not None, a boolean indicating if this file is +geospatial. If not specified or None, this will be checked.

  • +
  • _concurrency – the number of cpus to use during conversion. None to +use the logical cpu count.

  • +
+
+
Returns:
+

outputPath if successful

+
+
+
+ +
+
+large_image_converter.format_hook(funcname, *args, **kwargs)[source]🔗
+

Call a function specific to a file format.

+
+
Parameters:
+
    +
  • funcname – name of the function.

  • +
  • args – parameters to pass to the function.

  • +
  • kwargs – parameters to pass to the function.

  • +
+
+
Returns:
+

dependent on the function. False to indicate no further +processing should be done.

+
+
+
+ +
+
+large_image_converter.is_geospatial(path)[source]🔗
+

Check if a path is likely to be a geospatial file.

+
+
Parameters:
+

path – The path to the file

+
+
Returns:
+

True if geospatial.

+
+
+
+ +
+
+large_image_converter.is_vips(path, matchSize=None)[source]🔗
+

Check if a path is readable by vips.

+
+
Parameters:
+
    +
  • path – The path to the file

  • +
  • matchSize – if not None, the image read by vips must be the specified +(width, height) tuple in pixels.

  • +
+
+
Returns:
+

True if readable by vips.

+
+
+
+ +
+
+large_image_converter.json_serial(obj)[source]🔗
+

Fallback serializier for json. This serializes datetime objects to iso +format.

+
+
Parameters:
+

obj – an object to serialize.

+
+
Returns:
+

a serialized string.

+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_converter/modules.html b/_build/large_image_converter/modules.html new file mode 100644 index 000000000..70c45116d --- /dev/null +++ b/_build/large_image_converter/modules.html @@ -0,0 +1,175 @@ + + + + + + + large_image_converter — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_bioformats/large_image_source_bioformats.html b/_build/large_image_source_bioformats/large_image_source_bioformats.html new file mode 100644 index 000000000..e3d011f38 --- /dev/null +++ b/_build/large_image_source_bioformats/large_image_source_bioformats.html @@ -0,0 +1,349 @@ + + + + + + + large_image_source_bioformats package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_bioformats package🔗

+
+

Submodules🔗

+
+
+

large_image_source_bioformats.girder_source module🔗

+
+
+class large_image_source_bioformats.girder_source.BioformatsGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: BioformatsFileTileSource, GirderTileSource

+

Provides tile access to Girder items that can be read with bioformats.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – the associated file path.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+mayHaveAdjacentFiles(largeImageFile)[source]🔗
+
+ +
+
+name = 'bioformats'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_bioformats.BioformatsFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to via Bioformats.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – the associated file path.

+
+
+
+
+classmethod addKnownExtensions()[source]🔗
+
+ +
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'czi': SourcePriority.PREFERRED, 'ets': SourcePriority.LOW, 'lif': SourcePriority.MEDIUM, 'vsi': SourcePriority.PREFERRED, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+getAssociatedImagesList()[source]🔗
+

Return a list of associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/czi': SourcePriority.PREFERRED, 'image/vsi': SourcePriority.PREFERRED, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'bioformats'🔗
+
+ +
+ +
+
+large_image_source_bioformats.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_bioformats.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_bioformats/modules.html b/_build/large_image_source_bioformats/modules.html new file mode 100644 index 000000000..fc44d5b2c --- /dev/null +++ b/_build/large_image_source_bioformats/modules.html @@ -0,0 +1,191 @@ + + + + + + + large_image_source_bioformats — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_deepzoom/large_image_source_deepzoom.html b/_build/large_image_source_deepzoom/large_image_source_deepzoom.html new file mode 100644 index 000000000..d0cfe1951 --- /dev/null +++ b/_build/large_image_source_deepzoom/large_image_source_deepzoom.html @@ -0,0 +1,309 @@ + + + + + + + large_image_source_deepzoom package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_deepzoom package🔗

+
+

Submodules🔗

+
+
+

large_image_source_deepzoom.girder_source module🔗

+
+
+class large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: DeepzoomFileTileSource, GirderTileSource

+

Deepzoom large_image tile source for Girder.

+

Provides tile access to Girder items with a Deepzoom xml (dzi) file and +associated pngs/jpegs in relative folders and items or on the local file +system.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'deepzoom'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_deepzoom.DeepzoomFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to a Deepzoom xml (dzi) file and associated pngs/jpegs +in relative folders on the local file system.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'dzc': SourcePriority.HIGH, 'dzi': SourcePriority.HIGH, None: SourcePriority.LOW}🔗
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'deepzoom'🔗
+
+ +
+ +
+
+large_image_source_deepzoom.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_deepzoom.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_deepzoom/modules.html b/_build/large_image_source_deepzoom/modules.html new file mode 100644 index 000000000..9679db1af --- /dev/null +++ b/_build/large_image_source_deepzoom/modules.html @@ -0,0 +1,186 @@ + + + + + + + large_image_source_deepzoom — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_dicom/large_image_source_dicom.assetstore.html b/_build/large_image_source_dicom/large_image_source_dicom.assetstore.html new file mode 100644 index 000000000..ff8229b71 --- /dev/null +++ b/_build/large_image_source_dicom/large_image_source_dicom.assetstore.html @@ -0,0 +1,512 @@ + + + + + + + large_image_source_dicom.assetstore package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_dicom.assetstore package🔗

+
+

Submodules🔗

+
+
+

large_image_source_dicom.assetstore.dicomweb_assetstore_adapter module🔗

+
+
+class large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter(assetstore)[source]🔗
+

Bases: AbstractAssetstoreAdapter

+

This defines the interface to be used by all assetstore adapters.

+
+
+property assetstore_meta🔗
+
+ +
+
+property auth_session🔗
+
+ +
+
+deleteFile(file)[source]🔗
+

This is called when a File is deleted to allow the adapter to remove +the data from within the assetstore. This method should not modify +or delete the file object, as the caller will delete it afterward.

+
+
Parameters:
+

file (dict) – The File document about to be deleted.

+
+
+
+ +
+
+downloadFile(file, offset=0, headers=True, endByte=None, contentDisposition=None, extraParameters=None, **kwargs)[source]🔗
+

This method is in charge of returning a value to the RESTful endpoint +that can be used to download the file. This should either return a +generator function that yields the bytes of the file (which will stream +the file directly), or modify the response headers and raise a +cherrypy.HTTPRedirect.

+
+
Parameters:
+
    +
  • file (dict) – The file document being downloaded.

  • +
  • offset (int) – Offset in bytes to start the download at.

  • +
  • headers (bool) – Flag for whether headers should be sent on the response.

  • +
  • endByte (int or None) – Final byte to download. If None, downloads to the +end of the file.

  • +
  • contentDisposition (str or None) – Value for Content-Disposition response +header disposition-type value.

  • +
+
+
+
+ +
+
+finalizeUpload(upload, file)[source]🔗
+

Call this once the last chunk has been processed. This method does not +need to delete the upload document as that will be deleted by the +caller afterward. This method may augment the File document, and must +return the File document.

+
+
Parameters:
+
    +
  • upload (dict) – The upload document.

  • +
  • file (dict) – The file document that was created.

  • +
+
+
Returns:
+

The file document with optional modifications.

+
+
+
+ +
+
+getFileSize(file)[source]🔗
+

Get the file size (computing it, if necessary). Default behavior simply +returns file.get(‘size’, 0). This method exists because some +assetstores do not compute the file size immediately, but only when +it is actually needed. The assetstore may also need to update the file +size after some changes.

+
+ +
+
+importData(parent, parentType, params, progress, user, **kwargs)[source]🔗
+

Import DICOMweb WSI instances from a DICOMweb server.

+
+
Parameters:
+
    +
  • parent – The parent object to import into.

  • +
  • parentType (str) – The model type of the parent object.

  • +
  • params (dict) –

    Additional parameters required for the import process. +This dictionary may include the following keys:

    +
    +
    limit:
    +

    (optional) limit the number of studies imported.

    +
    +
    filters:
    +

    (optional) a dictionary/JSON string of additional search +filters to use with dicomweb_client’s search_for_series() +function.

    +
    +
    +

  • +
  • progress (girder.utility.progress.ProgressContext) – Object on which to record progress if possible.

  • +
  • user (dict or None) – The Girder user performing the import.

  • +
+
+
Returns:
+

a list of items that were created

+
+
+
+ +
+
+initUpload(upload)[source]🔗
+

This must be called before any chunks are uploaded to do any +additional behavior and optionally augment the upload document. The +method must return the upload document. Default behavior is to +simply return the upload document unmodified.

+
+
Parameters:
+

upload (dict) – The upload document to optionally augment.

+
+
+
+ +
+
+setContentHeaders(file, offset, endByte, contentDisposition=None)[source]🔗
+

Sets the Content-Length, Content-Disposition, Content-Type, and also +the Content-Range header if this is a partial download.

+
+
Parameters:
+
    +
  • file – The file being downloaded.

  • +
  • offset (int) – The start byte of the download.

  • +
  • endByte (int or None) – The end byte of the download (non-inclusive).

  • +
  • contentDisposition (str or None) – Content-Disposition response header +disposition-type value, if None, Content-Disposition will +be set to ‘attachment; filename=$filename’.

  • +
+
+
+
+ +
+
+static validateInfo(doc)[source]🔗
+

Adapters may implement this if they need to perform any validation +steps whenever the assetstore info is saved to the database. It should +return the document with any necessary alterations in the success case, +or throw an exception if validation fails.

+
+ +
+ +
+
+

large_image_source_dicom.assetstore.rest module🔗

+
+
+class large_image_source_dicom.assetstore.rest.DICOMwebAssetstoreResource[source]🔗
+

Bases: Resource

+
+
+importData(assetstore, destinationId, destinationType, limit, filters, progress)[source]🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter(assetstore)[source]🔗
+

Bases: AbstractAssetstoreAdapter

+

This defines the interface to be used by all assetstore adapters.

+
+
+property assetstore_meta🔗
+
+ +
+
+property auth_session🔗
+
+ +
+
+deleteFile(file)[source]🔗
+

This is called when a File is deleted to allow the adapter to remove +the data from within the assetstore. This method should not modify +or delete the file object, as the caller will delete it afterward.

+
+
Parameters:
+

file (dict) – The File document about to be deleted.

+
+
+
+ +
+
+downloadFile(file, offset=0, headers=True, endByte=None, contentDisposition=None, extraParameters=None, **kwargs)[source]🔗
+

This method is in charge of returning a value to the RESTful endpoint +that can be used to download the file. This should either return a +generator function that yields the bytes of the file (which will stream +the file directly), or modify the response headers and raise a +cherrypy.HTTPRedirect.

+
+
Parameters:
+
    +
  • file (dict) – The file document being downloaded.

  • +
  • offset (int) – Offset in bytes to start the download at.

  • +
  • headers (bool) – Flag for whether headers should be sent on the response.

  • +
  • endByte (int or None) – Final byte to download. If None, downloads to the +end of the file.

  • +
  • contentDisposition (str or None) – Value for Content-Disposition response +header disposition-type value.

  • +
+
+
+
+ +
+
+finalizeUpload(upload, file)[source]🔗
+

Call this once the last chunk has been processed. This method does not +need to delete the upload document as that will be deleted by the +caller afterward. This method may augment the File document, and must +return the File document.

+
+
Parameters:
+
    +
  • upload (dict) – The upload document.

  • +
  • file (dict) – The file document that was created.

  • +
+
+
Returns:
+

The file document with optional modifications.

+
+
+
+ +
+
+getFileSize(file)[source]🔗
+

Get the file size (computing it, if necessary). Default behavior simply +returns file.get(‘size’, 0). This method exists because some +assetstores do not compute the file size immediately, but only when +it is actually needed. The assetstore may also need to update the file +size after some changes.

+
+ +
+
+importData(parent, parentType, params, progress, user, **kwargs)[source]🔗
+

Import DICOMweb WSI instances from a DICOMweb server.

+
+
Parameters:
+
    +
  • parent – The parent object to import into.

  • +
  • parentType (str) – The model type of the parent object.

  • +
  • params (dict) –

    Additional parameters required for the import process. +This dictionary may include the following keys:

    +
    +
    limit:
    +

    (optional) limit the number of studies imported.

    +
    +
    filters:
    +

    (optional) a dictionary/JSON string of additional search +filters to use with dicomweb_client’s search_for_series() +function.

    +
    +
    +

  • +
  • progress (girder.utility.progress.ProgressContext) – Object on which to record progress if possible.

  • +
  • user (dict or None) – The Girder user performing the import.

  • +
+
+
Returns:
+

a list of items that were created

+
+
+
+ +
+
+initUpload(upload)[source]🔗
+

This must be called before any chunks are uploaded to do any +additional behavior and optionally augment the upload document. The +method must return the upload document. Default behavior is to +simply return the upload document unmodified.

+
+
Parameters:
+

upload (dict) – The upload document to optionally augment.

+
+
+
+ +
+
+setContentHeaders(file, offset, endByte, contentDisposition=None)[source]🔗
+

Sets the Content-Length, Content-Disposition, Content-Type, and also +the Content-Range header if this is a partial download.

+
+
Parameters:
+
    +
  • file – The file being downloaded.

  • +
  • offset (int) – The start byte of the download.

  • +
  • endByte (int or None) – The end byte of the download (non-inclusive).

  • +
  • contentDisposition (str or None) – Content-Disposition response header +disposition-type value, if None, Content-Disposition will +be set to ‘attachment; filename=$filename’.

  • +
+
+
+
+ +
+
+static validateInfo(doc)[source]🔗
+

Adapters may implement this if they need to perform any validation +steps whenever the assetstore info is saved to the database. It should +return the document with any necessary alterations in the success case, +or throw an exception if validation fails.

+
+ +
+ +
+
+large_image_source_dicom.assetstore.load(info)[source]🔗
+

Load the plugin into Girder.

+
+
Parameters:
+

info – a dictionary of plugin information. The name key contains the +name of the plugin according to Girder.

+
+
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_dicom/large_image_source_dicom.html b/_build/large_image_source_dicom/large_image_source_dicom.html new file mode 100644 index 000000000..73de54a39 --- /dev/null +++ b/_build/large_image_source_dicom/large_image_source_dicom.html @@ -0,0 +1,483 @@ + + + + + + + large_image_source_dicom package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_dicom package🔗

+
+

Subpackages🔗

+ +
+
+

Submodules🔗

+
+
+

large_image_source_dicom.dicom_metadata module🔗

+
+
+large_image_source_dicom.dicom_metadata.extract_dicom_metadata(dataset)[source]🔗
+
+ +
+
+large_image_source_dicom.dicom_metadata.extract_specimen_metadata(dataset)[source]🔗
+
+ +
+
+

large_image_source_dicom.dicom_tags module🔗

+
+
+large_image_source_dicom.dicom_tags.dicom_key_to_tag(key)[source]🔗
+
+ +
+
+

large_image_source_dicom.dicomweb_utils module🔗

+
+
+large_image_source_dicom.dicomweb_utils.get_dicomweb_metadata(client, study_uid, series_uid)[source]🔗
+
+ +
+
+large_image_source_dicom.dicomweb_utils.get_first_wsi_volume_metadata(client, study_uid, series_uid)[source]🔗
+
+ +
+
+

large_image_source_dicom.girder_plugin module🔗

+
+
+class large_image_source_dicom.girder_plugin.DICOMwebPlugin(entrypoint)[source]🔗
+

Bases: GirderPlugin

+
+
+CLIENT_SOURCE_PATH = 'web_client'🔗
+

The path of the plugin’s web client source code. This path is given relative to the python +package. This property is used to link the web client source into the staging area while +building in development mode. When this value is None it indicates there is no web client +component.

+
+ +
+
+DISPLAY_NAME = 'DICOMweb Plugin'🔗
+

This is the named displayed to users on the plugin page. Unlike the entrypoint name +used internally, this name can be an arbitrary string.

+
+ +
+
+load(info)[source]🔗
+
+ +
+ +
+
+

large_image_source_dicom.girder_source module🔗

+
+
+class large_image_source_dicom.girder_source.DICOMGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: DICOMFileTileSource, GirderTileSource

+

Provides tile access to Girder items with an DICOM file or other files that +the dicomreader library can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'dicom'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_dicom.DICOMFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to dicom files the dicom or dicomreader library can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'dcm': SourcePriority.PREFERRED, 'dic': SourcePriority.PREFERRED, 'dicom': SourcePriority.PREFERRED, None: SourcePriority.LOW}🔗
+
+ +
+
+getAssociatedImagesList()[source]🔗
+

Return a list of associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'application/dicom': SourcePriority.PREFERRED, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'dicom'🔗
+
+ +
+
+nameMatches: Dict[str, SourcePriority] = {'DCM_\\d+$': SourcePriority.MEDIUM, '\\d+(\\.\\d+){3,20}$': SourcePriority.MEDIUM}🔗
+
+ +
+ +
+
+large_image_source_dicom.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_dicom.dicom_to_dict(ds, base=None)[source]🔗
+

Convert a pydicom dataset to a fairly flat python dictionary for purposes +of reporting. This is not invertable without extra work.

+
+
Parameters:
+
    +
  • ds – a pydicom dataset.

  • +
  • base – a base dataset entry within the dataset.

  • +
+
+
Returns:
+

a dictionary of values.

+
+
+
+ +
+
+large_image_source_dicom.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_dicom/modules.html b/_build/large_image_source_dicom/modules.html new file mode 100644 index 000000000..ba383ff3a --- /dev/null +++ b/_build/large_image_source_dicom/modules.html @@ -0,0 +1,224 @@ + + + + + + + large_image_source_dicom — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_dummy/large_image_source_dummy.html b/_build/large_image_source_dummy/large_image_source_dummy.html new file mode 100644 index 000000000..ef532e343 --- /dev/null +++ b/_build/large_image_source_dummy/large_image_source_dummy.html @@ -0,0 +1,332 @@ + + + + + + + large_image_source_dummy package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_dummy package🔗

+
+

Module contents🔗

+
+
+class large_image_source_dummy.DummyTileSource(*args, **kwargs)[source]🔗
+

Bases: TileSource

+

Initialize the tile class.

+
+
Parameters:
+
    +
  • jpegQuality – when serving jpegs, use this quality.

  • +
  • jpegSubsampling – when serving jpegs, use this subsampling (0 is +full chroma, 1 is half, 2 is quarter).

  • +
  • encoding – ‘JPEG’, ‘PNG’, ‘TIFF’, or ‘TILED’.

  • +
  • edge – False to leave edge tiles whole, True or ‘crop’ to crop +edge tiles, otherwise, an #rrggbb color to fill edges.

  • +
  • tiffCompression – the compression format to use when encoding a +TIFF.

  • +
  • style

    if None, use the default style for the file. Otherwise, +this is a string with a json-encoded dictionary. The style can +contain the following keys:

    +
    +
    +
    band:
    +

    if -1 or None, and if style is specified at all, the +greyscale value is used. Otherwise, a 1-based numerical +index into the channels of the image or a string that +matches the interpretation of the band (‘red’, ‘green’, +‘blue’, ‘gray’, ‘alpha’). Note that ‘gray’ on an RGB or +RGBA image will use the green band.

    +
    +
    frame:
    +

    if specified, override the frame value for this band. +When used as part of a bands list, this can be used to +composite multiple frames together. It is most efficient +if at least one band either doesn’t specify a frame +parameter or specifies the same frame value as the primary +query.

    +
    +
    framedelta:
    +

    if specified and frame is not specified, override +the frame value for this band by using the current frame +plus this value.

    +
    +
    min:
    +

    the value to map to the first palette value. Defaults to +0. ‘auto’ to use 0 if the reported minimum and maximum of +the band are between [0, 255] or use the reported minimum +otherwise. ‘min’ or ‘max’ to always uses the reported +minimum or maximum. ‘full’ to always use 0.

    +
    +
    max:
    +

    the value to map to the last palette value. Defaults to +255. ‘auto’ to use 0 if the reported minimum and maximum +of the band are between [0, 255] or use the reported +maximum otherwise. ‘min’ or ‘max’ to always uses the +reported minimum or maximum. ‘full’ to use the maximum +value of the base data type (either 1, 255, or 65535).

    +
    +
    palette:
    +

    a single color string, a palette name, or a list of +two or more color strings. Color strings are of the form +#RRGGBB, #RRGGBBAA, #RGB, #RGBA, or any string parseable by +the PIL modules, or, if it is installed, by matplotlib. A +single color string is the same as the list [‘#000’, +<color>]. Palette names are the name of a palettable +palette or, if available, a matplotlib palette.

    +
    +
    nodata:
    +

    the value to use for missing data. null or unset to +not use a nodata value.

    +
    +
    composite:
    +

    either ‘lighten’ or ‘multiply’. Defaults to +‘lighten’ for all except the alpha band.

    +
    +
    clamp:
    +

    either True to clamp (also called clip or crop) values +outside of the [min, max] to the ends of the palette or +False to make outside values transparent.

    +
    +
    dtype:
    +

    convert the results to the specified numpy dtype. +Normally, if a style is applied, the results are +intermediately a float numpy array with a value range of +[0,255]. If this is ‘uint16’, it will be cast to that and +multiplied by 65535/255. If ‘float’, it will be divided by +255. If ‘source’, this uses the dtype of the source image.

    +
    +
    axis:
    +

    keep only the specified axis from the numpy intermediate +results. This can be used to extract a single channel +after compositing.

    +
    +
    +
    +

    Alternately, the style object can contain a single key of ‘bands’, +which has a value which is a list of style dictionaries as above, +excepting that each must have a band that is not -1. Bands are +composited in the order listed. This base object may also contain +the ‘dtype’ and ‘axis’ values.

    +

  • +
  • noCache – if True, the style can be adjusted dynamically and the +source is not elibible for caching. If there is no intention to +reuse the source at a later time, this can have performance +benefits, such as when first cataloging images that can be read.

  • +
+
+
+
+
+classmethod canRead(*args, **kwargs)[source]🔗
+

Check if we can read the input. This takes the same parameters as +__init__.

+
+
Returns:
+

True if this class can read the input. False if it cannot.

+
+
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {None: SourcePriority.MANUAL}🔗
+
+ +
+
+getTile(x, y, z, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+name = 'dummy'🔗
+
+ +
+ +
+
+large_image_source_dummy.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_dummy.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_dummy/modules.html b/_build/large_image_source_dummy/modules.html new file mode 100644 index 000000000..30e489c0e --- /dev/null +++ b/_build/large_image_source_dummy/modules.html @@ -0,0 +1,170 @@ + + + + + + + large_image_source_dummy — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_gdal/large_image_source_gdal.html b/_build/large_image_source_gdal/large_image_source_gdal.html new file mode 100644 index 000000000..276bc24e7 --- /dev/null +++ b/_build/large_image_source_gdal/large_image_source_gdal.html @@ -0,0 +1,711 @@ + + + + + + + large_image_source_gdal package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_gdal package🔗

+
+

Submodules🔗

+
+
+

large_image_source_gdal.girder_source module🔗

+
+
+class large_image_source_gdal.girder_source.GDALGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: GDALFileTileSource, GirderTileSource

+

Provides tile access to Girder items for gdal layers.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+
    +
  • path – a filesystem path for the tile source.

  • +
  • projection – None to use pixel space, otherwise a proj4 +projection string or a case-insensitive string of the form +‘EPSG:<epsg number>’. If a string and case-insensitively prefixed +with ‘proj4:’, that prefix is removed. For instance, +‘proj4:EPSG:3857’, ‘PROJ4:+init=epsg:3857’, and ‘+init=epsg:3857’, +and ‘EPSG:3857’ are all equivalent.

  • +
  • unitsPerPixel – The size of a pixel at the 0 tile size. Ignored +if the projection is None. For projections, None uses the default, +which is the distance between (-180,0) and (180,0) in EPSG:4326 +converted to the projection divided by the tile size. Proj4 +projections that are not latlong (is_geographic is False) must +specify unitsPerPixel.

  • +
+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+static getLRUHash(*args, **kwargs)[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'gdal'🔗
+
+ +
+
+projection: str | bytes🔗
+
+ +
+
+projectionOrigin: Tuple[float, float]🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+sourceLevels: int🔗
+
+ +
+
+sourceSizeX: int🔗
+
+ +
+
+sourceSizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+
+unitsAcrossLevel0: float🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_gdal.GDALFileTileSource(*args, **kwargs)[source]🔗
+

Bases: GDALBaseFileTileSource

+

Provides tile access to geospatial files.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+
    +
  • path – a filesystem path for the tile source.

  • +
  • projection – None to use pixel space, otherwise a proj4 +projection string or a case-insensitive string of the form +‘EPSG:<epsg number>’. If a string and case-insensitively prefixed +with ‘proj4:’, that prefix is removed. For instance, +‘proj4:EPSG:3857’, ‘PROJ4:+init=epsg:3857’, and ‘+init=epsg:3857’, +and ‘EPSG:3857’ are all equivalent.

  • +
  • unitsPerPixel – The size of a pixel at the 0 tile size. Ignored +if the projection is None. For projections, None uses the default, +which is the distance between (-180,0) and (180,0) in EPSG:4326 +converted to the projection divided by the tile size. Proj4 +projections that are not latlong (is_geographic is False) must +specify unitsPerPixel.

  • +
+
+
+
+
+classmethod addKnownExtensions()[source]🔗
+
+ +
+
+cacheName = 'tilesource'🔗
+
+ +
+
+property geospatial🔗
+

This is true if the source has geospatial information.

+
+ +
+
+getBandInformation(statistics=True, dataset=None, **kwargs)[source]🔗
+

Get information about each band in the image.

+
+
Parameters:
+
    +
  • statistics – if True, compute statistics if they don’t already +exist. Ignored: always treated as True.

  • +
  • dataset – the dataset. If None, use the main dataset.

  • +
+
+
Returns:
+

a list of one dictionary per band. Each dictionary contains +known values such as interpretation, min, max, mean, stdev, nodata, +scale, offset, units, categories, colortable, maskband.

+
+
+
+ +
+
+getBounds(srs=None)[source]🔗
+

Returns bounds of the image.

+
+
Parameters:
+

srs – the projection for the bounds. None for the default 4326.

+
+
Returns:
+

an object with the four corners and the projection that was +used. None if we don’t know the original projection.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs)[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return metadata about this tile source. This contains

+
+
+
levels:
+

number of tile levels in this image.

+
+
sizeX:
+

width of the image in pixels.

+
+
sizeY:
+

height of the image in pixels.

+
+
tileWidth:
+

width of a tile in pixels.

+
+
tileHeight:
+

height of a tile in pixels.

+
+
magnification:
+

if known, the magnificaiton of the image.

+
+
mm_x:
+

if known, the width of a pixel in millimeters.

+
+
mm_y:
+

if known, the height of a pixel in millimeters.

+
+
dtype:
+

if known, the type of values in this image.

+
+
+

In addition to the keys that listed above, tile sources that expose +multiple frames will also contain

+
+
frames:
+

a list of frames. Each frame entry is a dictionary with

+
+
Frame:
+

a 0-values frame index (the location in the list)

+
+
Channel:
+

optional. The name of the channel, if known

+
+
IndexC:
+

optional if unique. A 0-based index into the channel +list

+
+
IndexT:
+

optional if unique. A 0-based index for time values

+
+
IndexZ:
+

optional if unique. A 0-based index for z values

+
+
IndexXY:
+

optional if unique. A 0-based index for view (xy) +values

+
+
Index<axis>:
+

optional if unique. A 0-based index for an +arbitrary axis.

+
+
Index:
+

a 0-based index of non-channel unique sets. If the +frames vary only by channel and are adjacent, they will +have the same index.

+
+
+
+
IndexRange:
+

a dictionary of the number of unique index values from +frames if greater than 1 (e.g., if an entry like IndexXY is not +present, then all frames either do not have that value or have +a value of 0).

+
+
IndexStride:
+

a dictionary of the spacing between frames where +unique axes values change.

+
+
channels:
+

optional. If known, a list of channel names

+
+
channelmap:
+

optional. If known, a dictionary of channel names +with their offset into the channel list.

+
+
+
+

Note that this does not include band information, though some tile +sources may do so.

+
+ +
+
+getPixel(**kwargs)[source]🔗
+

Get a single pixel from the current tile source.

+
+
Parameters:
+

kwargs – optional arguments. Some options are region, output, +encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See +tileIterator.

+
+
Returns:
+

a dictionary with the value of the pixel for each channel on +a scale of [0-255], including alpha, if available. This may +contain additional information.

+
+
+
+ +
+
+getProj4String()[source]🔗
+

Returns proj4 string for the given dataset

+
+
Returns:
+

The proj4 string or None.

+
+
+
+ +
+
+getRegion(format=('image',), **kwargs)[source]🔗
+

Get a rectangular region from the current tile source. Aspect ratio is +preserved. If neither width nor height is given, the original size of +the highest resolution level is used. If both are given, the returned +image will be no larger than either size.

+
+
Parameters:
+
    +
  • format – the desired format or a tuple of allowed formats. +Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, +TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding may be +specified.

  • +
  • kwargs – optional arguments. Some options are region, output, +encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See +tileIterator.

  • +
+
+
Returns:
+

regionData, formatOrRegionMime: the image data and either the +mime type, if the format is TILE_FORMAT_IMAGE, or the format.

+
+
+
+ +
+
+getState()[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+static isGeospatial(path)[source]🔗
+

Check if a path is likely to be a geospatial file.

+
+
Parameters:
+

path – The path to the file

+
+
Returns:
+

True if geospatial.

+
+
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'gdal'🔗
+
+ +
+
+pixelToProjection(x, y, level=None)[source]🔗
+

Convert from pixels back to projection coordinates.

+
+
Parameters:
+
    +
  • y (x,) – base pixel coordinates.

  • +
  • level – the level of the pixel. None for maximum level.

  • +
+
+
Returns:
+

x, y in projection coordinates.

+
+
+
+ +
+
+projection: str | bytes🔗
+
+ +
+
+projectionOrigin: Tuple[float, float]🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+sourceLevels: int🔗
+
+ +
+
+sourceSizeX: int🔗
+
+ +
+
+sourceSizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+
+toNativePixelCoordinates(x, y, proj=None, roundResults=True)[source]🔗
+

Convert a coordinate in the native projection (self.getProj4String) to +pixel coordinates.

+
+
Parameters:
+
    +
  • x – the x coordinate it the native projection.

  • +
  • y – the y coordinate it the native projection.

  • +
  • proj – input projection. None to use the source’s projection.

  • +
  • roundResults – if True, round the results to the nearest pixel.

  • +
+
+
Returns:
+

(x, y) the pixel coordinate.

+
+
+
+ +
+
+unitsAcrossLevel0: float🔗
+
+ +
+
+validateCOG(check_tiled=True, full_check=False, strict=True, warn=True)[source]🔗
+

Check if this image is a valid Cloud Optimized GeoTiff.

+

This will raise a large_image.exceptions.TileSourceInefficientError +if not a valid Cloud Optimized GeoTiff. Otherwise, returns True.

+

Requires the osgeo_utils package.

+
+
Parameters:
+
    +
  • check_tiled (bool) – Set to False to ignore missing tiling.

  • +
  • full_check (bool) – Set to True to check tile/strip leader/trailer bytes. +Might be slow on remote files

  • +
  • strict (bool) – Enforce warnings as exceptions. Set to False to only warn and not +raise exceptions.

  • +
  • warn (bool) – Log any warnings

  • +
+
+
+
+ +
+ +
+
+large_image_source_gdal.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_gdal.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_gdal/modules.html b/_build/large_image_source_gdal/modules.html new file mode 100644 index 000000000..a04db4a0a --- /dev/null +++ b/_build/large_image_source_gdal/modules.html @@ -0,0 +1,216 @@ + + + + + + + large_image_source_gdal — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_gdal🔗

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_mapnik/large_image_source_mapnik.html b/_build/large_image_source_mapnik/large_image_source_mapnik.html new file mode 100644 index 000000000..2216fd019 --- /dev/null +++ b/_build/large_image_source_mapnik/large_image_source_mapnik.html @@ -0,0 +1,477 @@ + + + + + + + large_image_source_mapnik package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_mapnik package🔗

+
+

Submodules🔗

+
+
+

large_image_source_mapnik.girder_source module🔗

+
+
+class large_image_source_mapnik.girder_source.MapnikGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: MapnikFileTileSource, GDALGirderTileSource

+

Provides tile access to Girder items for mapnik layers.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+
    +
  • path – a filesystem path for the tile source.

  • +
  • projection – None to use pixel space, otherwise a proj4 +projection string or a case-insensitive string of the form +‘EPSG:<epsg number>’. If a string and case-insensitively prefixed +with ‘proj4:’, that prefix is removed. For instance, +‘proj4:EPSG:3857’, ‘PROJ4:+init=epsg:3857’, and ‘+init=epsg:3857’, +and ‘EPSG:3857’ are all equivalent.

  • +
  • style

    if None, use the default style for the file. Otherwise, +this is a string with a json-encoded dictionary. The style is +ignored if it does not contain ‘band’ or ‘bands’. In addition to +the base class parameters, the style can also contain the following +keys:

    +
    +
    +
    scheme: one of the mapnik.COLORIZER_xxx values. Case

    insensitive. Possible values are at least ‘discrete’, +‘linear’, and ‘exact’. This defaults to ‘linear’.

    +
    +
    composite: this is a string containing one of the mapnik

    CompositeOp properties. It defaults to ‘lighten’.

    +
    +
    +
    +

  • +
  • unitsPerPixel – The size of a pixel at the 0 tile size. Ignored +if the projection is None. For projections, None uses the default, +which is the distance between (-180,0) and (180,0) in EPSG:4326 +converted to the projection divided by the tile size. Proj4 +projections that are not latlong (is_geographic is False) must +specify unitsPerPixel.

  • +
+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'mapnik'🔗
+
+ +
+
+projection: str | bytes🔗
+
+ +
+
+projectionOrigin: Tuple[float, float]🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+sourceLevels: int🔗
+
+ +
+
+sourceSizeX: int🔗
+
+ +
+
+sourceSizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+
+unitsAcrossLevel0: float🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_mapnik.MapnikFileTileSource(*args, **kwargs)[source]🔗
+

Bases: GDALFileTileSource

+

Provides tile access to geospatial files.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+
    +
  • path – a filesystem path for the tile source.

  • +
  • projection – None to use pixel space, otherwise a proj4 +projection string or a case-insensitive string of the form +‘EPSG:<epsg number>’. If a string and case-insensitively prefixed +with ‘proj4:’, that prefix is removed. For instance, +‘proj4:EPSG:3857’, ‘PROJ4:+init=epsg:3857’, and ‘+init=epsg:3857’, +and ‘EPSG:3857’ are all equivalent.

  • +
  • style

    if None, use the default style for the file. Otherwise, +this is a string with a json-encoded dictionary. The style is +ignored if it does not contain ‘band’ or ‘bands’. In addition to +the base class parameters, the style can also contain the following +keys:

    +
    +
    +
    scheme: one of the mapnik.COLORIZER_xxx values. Case

    insensitive. Possible values are at least ‘discrete’, +‘linear’, and ‘exact’. This defaults to ‘linear’.

    +
    +
    composite: this is a string containing one of the mapnik

    CompositeOp properties. It defaults to ‘lighten’.

    +
    +
    +
    +

  • +
  • unitsPerPixel – The size of a pixel at the 0 tile size. Ignored +if the projection is None. For projections, None uses the default, +which is the distance between (-180,0) and (180,0) in EPSG:4326 +converted to the projection divided by the tile size. Proj4 +projections that are not latlong (is_geographic is False) must +specify unitsPerPixel.

  • +
+
+
+
+
+addStyle(m, layerSrs, extent=None)[source]🔗
+

Attaches raster style option to mapnik raster layer and adds the layer +to the mapnik map.

+
+
Parameters:
+
    +
  • m – mapnik map.

  • +
  • layerSrs – the layer projection

  • +
  • extent – the extent to use for the mapnik layer.

  • +
+
+
+
+ +
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'nc': SourcePriority.PREFERRED, 'nitf': SourcePriority.HIGHER, 'ntf': SourcePriority.HIGHER, 'tif': SourcePriority.LOWER, 'tiff': SourcePriority.LOWER, 'vrt': SourcePriority.HIGHER, None: SourcePriority.LOW}🔗
+
+ +
+
+getOneBandInformation(band)[source]🔗
+

Get band information for a single band.

+
+
Parameters:
+

band – a 1-based band.

+
+
Returns:
+

a dictionary of band information. See getBandInformation.

+
+
+
+ +
+
+getTile(x, y, z, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+static interpolateMinMax(start, stop, count)[source]🔗
+

Returns interpolated values for a given +start, stop and count

+
+
Returns:
+

List of interpolated values

+
+
+
+ +
+
+levels: int🔗
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/geotiff': SourcePriority.HIGHER, 'image/tiff': SourcePriority.LOWER, 'image/x-tiff': SourcePriority.LOWER, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'mapnik'🔗
+
+ +
+
+projection: str | bytes🔗
+
+ +
+
+projectionOrigin: Tuple[float, float]🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+sourceLevels: int🔗
+
+ +
+
+sourceSizeX: int🔗
+
+ +
+
+sourceSizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+
+unitsAcrossLevel0: float🔗
+
+ +
+ +
+
+large_image_source_mapnik.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_mapnik.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_mapnik/modules.html b/_build/large_image_source_mapnik/modules.html new file mode 100644 index 000000000..8782bb19d --- /dev/null +++ b/_build/large_image_source_mapnik/modules.html @@ -0,0 +1,205 @@ + + + + + + + large_image_source_mapnik — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_multi/large_image_source_multi.html b/_build/large_image_source_multi/large_image_source_multi.html new file mode 100644 index 000000000..231e7fa60 --- /dev/null +++ b/_build/large_image_source_multi/large_image_source_multi.html @@ -0,0 +1,359 @@ + + + + + + + large_image_source_multi package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_multi package🔗

+
+

Submodules🔗

+
+
+

large_image_source_multi.girder_source module🔗

+
+
+class large_image_source_multi.girder_source.MultiGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: MultiFileTileSource, GirderTileSource

+

Provides tile access to Girder items with files that the multi source can +read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'multi'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_multi.MultiFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to a composite of other tile sources.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'json': SourcePriority.PREFERRED, 'yaml': SourcePriority.PREFERRED, 'yml': SourcePriority.PREFERRED, None: SourcePriority.MEDIUM}🔗
+
+ +
+
+getAssociatedImage(imageKey, *args, **kwargs)[source]🔗
+

Return an associated image.

+
+
Parameters:
+
    +
  • imageKey – the key of the associated image to retrieve.

  • +
  • kwargs – optional arguments. Some options are width, height, +encoding, jpegQuality, jpegSubsampling, and tiffCompression.

  • +
+
+
Returns:
+

imageData, imageMime: the image data and the mime type, or +None if the associated image doesn’t exist.

+
+
+
+ +
+
+getAssociatedImagesList()[source]🔗
+

Return a list of associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values. Also, only the first 100 sources are used.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'application/json': SourcePriority.PREFERRED, 'application/yaml': SourcePriority.PREFERRED, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'multi'🔗
+
+ +
+ +
+
+large_image_source_multi.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_multi.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_multi/modules.html b/_build/large_image_source_multi/modules.html new file mode 100644 index 000000000..1b64e2b87 --- /dev/null +++ b/_build/large_image_source_multi/modules.html @@ -0,0 +1,190 @@ + + + + + + + large_image_source_multi — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_nd2/large_image_source_nd2.html b/_build/large_image_source_nd2/large_image_source_nd2.html new file mode 100644 index 000000000..2c6b45f15 --- /dev/null +++ b/_build/large_image_source_nd2/large_image_source_nd2.html @@ -0,0 +1,362 @@ + + + + + + + large_image_source_nd2 package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_nd2 package🔗

+
+

Submodules🔗

+
+
+

large_image_source_nd2.girder_source module🔗

+
+
+class large_image_source_nd2.girder_source.ND2GirderTileSource(*args, **kwargs)[source]🔗
+

Bases: ND2FileTileSource, GirderTileSource

+

Provides tile access to Girder items with an ND2 file or other files that +the nd2 library can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'nd2'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_nd2.ND2FileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to nd2 files the nd2 library can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'nd2': SourcePriority.PREFERRED, None: SourcePriority.LOW}🔗
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/nd2': SourcePriority.PREFERRED, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'nd2'🔗
+
+ +
+ +
+
+large_image_source_nd2.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_nd2.diffObj(obj1, obj2)[source]🔗
+

Given two objects, report the differences that exist in the first object +that are not in the second object.

+
+
Parameters:
+
    +
  • obj1 – the first object to compare. Only values present in this +object are returned.

  • +
  • obj2 – the second object to compare.

  • +
+
+
Returns:
+

a subset of obj1.

+
+
+
+ +
+
+large_image_source_nd2.namedtupleToDict(obj)[source]🔗
+

Convert a namedtuple to a plain dictionary.

+
+
Parameters:
+

obj – the object to convert

+
+
Returns:
+

a dictionary or the original object.

+
+
+
+ +
+
+large_image_source_nd2.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_nd2/modules.html b/_build/large_image_source_nd2/modules.html new file mode 100644 index 000000000..340118605 --- /dev/null +++ b/_build/large_image_source_nd2/modules.html @@ -0,0 +1,190 @@ + + + + + + + large_image_source_nd2 — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_ometiff/large_image_source_ometiff.html b/_build/large_image_source_ometiff/large_image_source_ometiff.html new file mode 100644 index 000000000..6cb144087 --- /dev/null +++ b/_build/large_image_source_ometiff/large_image_source_ometiff.html @@ -0,0 +1,344 @@ + + + + + + + large_image_source_ometiff package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_ometiff package🔗

+
+

Submodules🔗

+
+
+

large_image_source_ometiff.girder_source module🔗

+
+
+class large_image_source_ometiff.girder_source.OMETiffGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: OMETiffFileTileSource, GirderTileSource

+

Provides tile access to Girder items with an OMETiff file.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'ometiff'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_ometiff.OMETiffFileTileSource(*args, **kwargs)[source]🔗
+

Bases: TiffFileTileSource

+

Provides tile access to TIFF files.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'ome': SourcePriority.PREFERRED, 'tif': SourcePriority.MEDIUM, 'tiff': SourcePriority.MEDIUM, None: SourcePriority.LOW}🔗
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification for the highest-resolution level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getPreferredLevel(level)[source]🔗
+

Given a desired level (0 is minimum resolution, self.levels - 1 is max +resolution), return the level that contains actual data that is no +lower resolution.

+
+
Parameters:
+

level – desired level

+
+
Returns level:
+

a level with actual data that is no lower resolution.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, sparseFallback=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/tiff': SourcePriority.MEDIUM, 'image/x-tiff': SourcePriority.MEDIUM}🔗
+
+ +
+
+name = 'ometiff'🔗
+
+ +
+ +
+
+large_image_source_ometiff.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_ometiff.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_ometiff/modules.html b/_build/large_image_source_ometiff/modules.html new file mode 100644 index 000000000..70305a5a2 --- /dev/null +++ b/_build/large_image_source_ometiff/modules.html @@ -0,0 +1,189 @@ + + + + + + + large_image_source_ometiff — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_openjpeg/large_image_source_openjpeg.html b/_build/large_image_source_openjpeg/large_image_source_openjpeg.html new file mode 100644 index 000000000..fdcf10c60 --- /dev/null +++ b/_build/large_image_source_openjpeg/large_image_source_openjpeg.html @@ -0,0 +1,334 @@ + + + + + + + large_image_source_openjpeg package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_openjpeg package🔗

+
+

Submodules🔗

+
+
+

large_image_source_openjpeg.girder_source module🔗

+
+
+class large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: OpenjpegFileTileSource, GirderTileSource

+

Provides tile access to Girder items with a jp2 file or other files that +the openjpeg library can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+mayHaveAdjacentFiles(largeImageFile)[source]🔗
+
+ +
+
+name = 'openjpeg'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_openjpeg.OpenjpegFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to jp2 files and other files the openjpeg library can +read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'j2k': SourcePriority.PREFERRED, 'jp2': SourcePriority.PREFERRED, 'jpf': SourcePriority.PREFERRED, 'jpx': SourcePriority.PREFERRED, None: SourcePriority.MEDIUM}🔗
+
+ +
+
+getAssociatedImagesList()[source]🔗
+

Return a list of associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/jp2': SourcePriority.PREFERRED, 'image/jpx': SourcePriority.PREFERRED, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'openjpeg'🔗
+
+ +
+ +
+
+large_image_source_openjpeg.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_openjpeg.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_openjpeg/modules.html b/_build/large_image_source_openjpeg/modules.html new file mode 100644 index 000000000..920cca4b2 --- /dev/null +++ b/_build/large_image_source_openjpeg/modules.html @@ -0,0 +1,189 @@ + + + + + + + large_image_source_openjpeg — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_openslide/large_image_source_openslide.html b/_build/large_image_source_openslide/large_image_source_openslide.html new file mode 100644 index 000000000..3c136a15b --- /dev/null +++ b/_build/large_image_source_openslide/large_image_source_openslide.html @@ -0,0 +1,355 @@ + + + + + + + large_image_source_openslide package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_openslide package🔗

+
+

Submodules🔗

+
+
+

large_image_source_openslide.girder_source module🔗

+
+
+class large_image_source_openslide.girder_source.OpenslideGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: OpenslideFileTileSource, GirderTileSource

+

Provides tile access to Girder items with an SVS file or other files that +the openslide library can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensionsWithAdjacentFiles = {'mrxs'}🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+mimeTypesWithAdjacentFiles = {'image/mirax'}🔗
+
+ +
+
+name = 'openslide'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_openslide.OpenslideFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to SVS files and other files the openslide library can +read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'bif': SourcePriority.LOW, 'dcm': SourcePriority.LOW, 'ini': SourcePriority.LOW, 'mrxs': SourcePriority.PREFERRED, 'ndpi': SourcePriority.PREFERRED, 'scn': SourcePriority.LOW, 'svs': SourcePriority.PREFERRED, 'svslide': SourcePriority.PREFERRED, 'tif': SourcePriority.MEDIUM, 'tiff': SourcePriority.MEDIUM, 'vms': SourcePriority.HIGH, 'vmu': SourcePriority.HIGH, None: SourcePriority.MEDIUM}🔗
+
+ +
+
+getAssociatedImagesList()[source]🔗
+

Get a list of all associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getPreferredLevel(level)[source]🔗
+

Given a desired level (0 is minimum resolution, self.levels - 1 is max +resolution), return the level that contains actual data that is no +lower resolution.

+
+
Parameters:
+

level – desired level

+
+
Returns level:
+

a level with actual data that is no lower resolution.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/mirax': SourcePriority.PREFERRED, 'image/tiff': SourcePriority.MEDIUM, 'image/x-tiff': SourcePriority.MEDIUM, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'openslide'🔗
+
+ +
+ +
+
+large_image_source_openslide.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_openslide.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_openslide/modules.html b/_build/large_image_source_openslide/modules.html new file mode 100644 index 000000000..ca992ba1d --- /dev/null +++ b/_build/large_image_source_openslide/modules.html @@ -0,0 +1,191 @@ + + + + + + + large_image_source_openslide — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_pil/large_image_source_pil.html b/_build/large_image_source_pil/large_image_source_pil.html new file mode 100644 index 000000000..9f0be94a8 --- /dev/null +++ b/_build/large_image_source_pil/large_image_source_pil.html @@ -0,0 +1,452 @@ + + + + + + + large_image_source_pil package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_pil package🔗

+
+

Submodules🔗

+
+
+

large_image_source_pil.girder_source module🔗

+
+
+class large_image_source_pil.girder_source.PILGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: PILFileTileSource, GirderTileSource

+

Provides tile access to Girder items with a PIL file.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+
    +
  • path – the associated file path.

  • +
  • maxSize – either a number or an object with {‘width’: (width), +‘height’: height} in pixels. If None, the default max size is +used.

  • +
+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+defaultMaxSize()[source]🔗
+

Get the default max size from the config settings.

+
+
Returns:
+

the default max size.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs)[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getState()[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, mayRedirect=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'pil'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_pil.PILFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to single image PIL files.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+
    +
  • path – the associated file path.

  • +
  • maxSize – either a number or an object with {‘width’: (width), +‘height’: height} in pixels. If None, the default max size is +used.

  • +
+
+
+
+
+classmethod addKnownExtensions()[source]🔗
+
+ +
+
+cacheName = 'tilesource'🔗
+
+ +
+
+defaultMaxSize()[source]🔗
+

Get the default max size from the config settings.

+
+
Returns:
+

the default max size.

+
+
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'jpe': SourcePriority.LOW, 'jpeg': SourcePriority.LOW, 'jpg': SourcePriority.LOW, None: SourcePriority.FALLBACK_HIGH}🔗
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs)[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getState()[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, mayRedirect=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/jpeg': SourcePriority.LOW, None: SourcePriority.FALLBACK_HIGH}🔗
+
+ +
+
+name = 'pil'🔗
+
+ +
+ +
+
+large_image_source_pil.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_pil.getMaxSize(size=None, maxDefault=4096)[source]🔗
+

Get the maximum width and height that we allow for an image.

+
+
Parameters:
+
    +
  • size – the requested maximum size. This is either a number to use +for both width and height, or an object with {‘width’: (width), +‘height’: height} in pixels. If None, the default max size is used.

  • +
  • maxDefault – a default value to use for width and height.

  • +
+
+
Returns:
+

maxWidth, maxHeight in pixels. 0 means no images are allowed.

+
+
+
+ +
+
+large_image_source_pil.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_pil/modules.html b/_build/large_image_source_pil/modules.html new file mode 100644 index 000000000..e5e62d95e --- /dev/null +++ b/_build/large_image_source_pil/modules.html @@ -0,0 +1,196 @@ + + + + + + + large_image_source_pil — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_rasterio/large_image_source_rasterio.html b/_build/large_image_source_rasterio/large_image_source_rasterio.html new file mode 100644 index 000000000..7ee6369fd --- /dev/null +++ b/_build/large_image_source_rasterio/large_image_source_rasterio.html @@ -0,0 +1,695 @@ + + + + + + + large_image_source_rasterio package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_rasterio package🔗

+
+

Submodules🔗

+
+
+

large_image_source_rasterio.girder_source module🔗

+
+
+class large_image_source_rasterio.girder_source.RasterioGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: RasterioFileTileSource, GirderTileSource

+

Provides tile access to Girder items for rasterio layers.

+

Initialize the tile class.

+

See the base class for other available parameters.

+
+
Parameters:
+
    +
  • path – a filesystem path for the tile source.

  • +
  • projection – None to use pixel space, otherwise a crs compatible with rasterio’s CRS.

  • +
  • unitsPerPixel – The size of a pixel at the 0 tile size. +Ignored if the projection is None. For projections, None uses the default, +which is the distance between (-180,0) and (180,0) in EPSG:4326 converted to the +projection divided by the tile size. crs projections that are not latlong +(is_geographic is False) must specify unitsPerPixel.

  • +
+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+static getLRUHash(*args, **kwargs)[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'rasterio'🔗
+
+ +
+
+projection: str | bytes🔗
+
+ +
+
+projectionOrigin: Tuple[float, float]🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+sourceLevels: int🔗
+
+ +
+
+sourceSizeX: int🔗
+
+ +
+
+sourceSizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+
+unitsAcrossLevel0: float🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_rasterio.RasterioFileTileSource(*args, **kwargs)[source]🔗
+

Bases: GDALBaseFileTileSource

+

Provides tile access to geospatial files.

+

Initialize the tile class.

+

See the base class for other available parameters.

+
+
Parameters:
+
    +
  • path – a filesystem path for the tile source.

  • +
  • projection – None to use pixel space, otherwise a crs compatible with rasterio’s CRS.

  • +
  • unitsPerPixel – The size of a pixel at the 0 tile size. +Ignored if the projection is None. For projections, None uses the default, +which is the distance between (-180,0) and (180,0) in EPSG:4326 converted to the +projection divided by the tile size. crs projections that are not latlong +(is_geographic is False) must specify unitsPerPixel.

  • +
+
+
+
+
+classmethod addKnownExtensions()[source]🔗
+
+ +
+
+cacheName = 'tilesource'🔗
+
+ +
+
+getBandInformation(statistics=True, dataset=None, **kwargs)[source]🔗
+

Get information about each band in the image.

+
+
Parameters:
+
    +
  • statistics – if True, compute statistics if they don’t already exist. +Ignored: always treated as True.

  • +
  • dataset – the dataset. If None, use the main dataset.

  • +
+
+
Returns:
+

a list of one dictionary per band. Each dictionary contains +known values such as interpretation, min, max, mean, stdev, nodata, +scale, offset, units, categories, colortable, maskband.

+
+
+
+ +
+
+getBounds(crs=None, **kwargs)[source]🔗
+

Returns bounds of the image.

+
+
Parameters:
+

crs – the projection for the bounds. None for the default.

+
+
Returns:
+

an object with the four corners and the projection that was used. +None if we don’t know the original projection.

+
+
+
+ +
+
+getCrs()[source]🔗
+

Returns crs object for the given dataset

+
+
Returns:
+

The crs or None.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source.

+

Data returned from this method is not guaranteed to be in +any particular format or have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs)[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return metadata about this tile source. This contains

+
+
+
levels:
+

number of tile levels in this image.

+
+
sizeX:
+

width of the image in pixels.

+
+
sizeY:
+

height of the image in pixels.

+
+
tileWidth:
+

width of a tile in pixels.

+
+
tileHeight:
+

height of a tile in pixels.

+
+
magnification:
+

if known, the magnificaiton of the image.

+
+
mm_x:
+

if known, the width of a pixel in millimeters.

+
+
mm_y:
+

if known, the height of a pixel in millimeters.

+
+
dtype:
+

if known, the type of values in this image.

+
+
+

In addition to the keys that listed above, tile sources that expose +multiple frames will also contain

+
+
frames:
+

a list of frames. Each frame entry is a dictionary with

+
+
Frame:
+

a 0-values frame index (the location in the list)

+
+
Channel:
+

optional. The name of the channel, if known

+
+
IndexC:
+

optional if unique. A 0-based index into the channel +list

+
+
IndexT:
+

optional if unique. A 0-based index for time values

+
+
IndexZ:
+

optional if unique. A 0-based index for z values

+
+
IndexXY:
+

optional if unique. A 0-based index for view (xy) +values

+
+
Index<axis>:
+

optional if unique. A 0-based index for an +arbitrary axis.

+
+
Index:
+

a 0-based index of non-channel unique sets. If the +frames vary only by channel and are adjacent, they will +have the same index.

+
+
+
+
IndexRange:
+

a dictionary of the number of unique index values from +frames if greater than 1 (e.g., if an entry like IndexXY is not +present, then all frames either do not have that value or have +a value of 0).

+
+
IndexStride:
+

a dictionary of the spacing between frames where +unique axes values change.

+
+
channels:
+

optional. If known, a list of channel names

+
+
channelmap:
+

optional. If known, a dictionary of channel names +with their offset into the channel list.

+
+
+
+

Note that this does not include band information, though some tile +sources may do so.

+
+ +
+
+getPixel(**kwargs)[source]🔗
+

Get a single pixel from the current tile source.

+
+
Parameters:
+

kwargs – optional arguments. Some options are region, output, encoding, +jpegQuality, jpegSubsampling, tiffCompression, fill. See tileIterator.

+
+
Returns:
+

a dictionary with the value of the pixel for each channel on a +scale of [0-255], including alpha, if available. This may contain +additional information.

+
+
+
+ +
+
+getRegion(format=('image',), **kwargs)[source]🔗
+

Get region.

+

Get a rectangular region from the current tile source. Aspect ratio is preserved. +If neither width nor height is given, the original size of the highest +resolution level is used. If both are given, the returned image will be +no larger than either size.

+
+
Parameters:
+
    +
  • format – the desired format or a tuple of allowed formats. Formats +are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, TILE_FORMAT_IMAGE). +If TILE_FORMAT_IMAGE, encoding may be specified.

  • +
  • kwargs – optional arguments. Some options are region, output, encoding, +jpegQuality, jpegSubsampling, tiffCompression, fill. See tileIterator.

  • +
+
+
Returns:
+

regionData, formatOrRegionMime: the image data and either the +mime type, if the format is TILE_FORMAT_IMAGE, or the format.

+
+
+
+ +
+
+getState()[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+static isGeospatial(path)[source]🔗
+

Check if a path is likely to be a geospatial file.

+
+
Parameters:
+

path – The path to the file

+
+
Returns:
+

True if geospatial.

+
+
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'rasterio'🔗
+
+ +
+
+pixelToProjection(x, y, level=None)[source]🔗
+

Convert from pixels back to projection coordinates.

+
+
Parameters:
+
    +
  • y (x,) – base pixel coordinates.

  • +
  • level – the level of the pixel. None for maximum level.

  • +
+
+
Returns:
+

px, py in projection coordinates.

+
+
+
+ +
+
+projection: str | bytes🔗
+
+ +
+
+projectionOrigin: Tuple[float, float]🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+sourceLevels: int🔗
+
+ +
+
+sourceSizeX: int🔗
+
+ +
+
+sourceSizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+
+toNativePixelCoordinates(x, y, crs=None, roundResults=True)[source]🔗
+

Convert a coordinate in the native projection to pixel coordinates.

+
+
Parameters:
+
    +
  • x – the x coordinate it the native projection.

  • +
  • y – the y coordinate it the native projection.

  • +
  • crs – input projection. None to use the sources’s projection.

  • +
  • roundResults – if True, round the results to the nearest pixel.

  • +
+
+
Returns:
+

(x, y) the pixel coordinate.

+
+
+
+ +
+
+unitsAcrossLevel0: float🔗
+
+ +
+
+validateCOG(strict=True, warn=True)[source]🔗
+

Check if this image is a valid Cloud Optimized GeoTiff.

+

This will raise a large_image.exceptions.TileSourceInefficientError +if not a valid Cloud Optimized GeoTiff. Otherwise, returns True. Requires +the rio-cogeo lib.

+
+
Parameters:
+
    +
  • strict – Enforce warnings as exceptions. Set to False to only warn +and not raise exceptions.

  • +
  • warn – Log any warnings

  • +
+
+
Returns:
+

the validity of the cogtiff

+
+
+
+ +
+ +
+
+large_image_source_rasterio.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_rasterio.make_crs(projection)[source]🔗
+
+ +
+
+large_image_source_rasterio.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_rasterio/modules.html b/_build/large_image_source_rasterio/modules.html new file mode 100644 index 000000000..e385f7c0a --- /dev/null +++ b/_build/large_image_source_rasterio/modules.html @@ -0,0 +1,216 @@ + + + + + + + large_image_source_rasterio — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_rasterio🔗

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_test/large_image_source_test.html b/_build/large_image_source_test/large_image_source_test.html new file mode 100644 index 000000000..cdc74184e --- /dev/null +++ b/_build/large_image_source_test/large_image_source_test.html @@ -0,0 +1,357 @@ + + + + + + + large_image_source_test package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_test package🔗

+
+

Module contents🔗

+
+
+class large_image_source_test.TestTileSource(*args, **kwargs)[source]🔗
+

Bases: TileSource

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+
    +
  • ignored_path – for compatibility with FileTileSource.

  • +
  • minLevel – minimum tile level

  • +
  • maxLevel – maximum tile level. If both sizeX and sizeY are +specified, this value is ignored.

  • +
  • tileWidth – tile width in pixels

  • +
  • tileHeight – tile height in pixels

  • +
  • sizeX – image width in pixels at maximum level. Computed from +maxLevel and tileWidth if None.

  • +
  • sizeY – image height in pixels at maximum level. Computed from +maxLevel and tileHeight if None.

  • +
  • fractal – if True, and the tile size is square and a power of +two, draw a simple fractal on the tiles.

  • +
  • frames – if present, this is either a single number for generic +frames, a comma-separated list of c,z,t,xy, or a string of the +form ‘<axis>=<count>,<axis>=<count>,…’.

  • +
  • monochrome – if True, return single channel tiles.

  • +
  • bands – if present, a comma-separated list of band names. +Defaults to red,green,blue. Each band may optionally specify a +value range in the form “<band name>=<min val>-<max val>”. If any +ranges are specified, bands with no ranges will use the union of +the specified ranges. The internal dtype with be uint8, uint16, or +float depending on the union of the specified ranges. If no ranges +are specified at all, it is the same as 0-255.

  • +
+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+classmethod canRead(*args, **kwargs)[source]🔗
+

Check if we can read the input. This takes the same parameters as +__init__.

+
+
Returns:
+

True if this class can read the input. False if it cannot.

+
+
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {None: SourcePriority.MANUAL}🔗
+
+ +
+
+fractalTile(image, x, y, widthCount, color=(0, 0, 0))[source]🔗
+

Draw a simple fractal in a tile image.

+
+
Parameters:
+
    +
  • image – a Pil image to draw on. Modified.

  • +
  • x – the tile x position

  • +
  • y – the tile y position

  • +
  • widthCount – 2 ** z; the number of tiles across for a “full size” +image at this z level.

  • +
  • color – an rgb tuple on a scale of [0-255].

  • +
+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+static getLRUHash(*args, **kwargs)[source]🔗
+

Return a string hash used as a key in the recently-used cache for tile +sources.

+
+
Returns:
+

a string hash value.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getState()[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getTile(x, y, z, *args, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'test'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+large_image_source_test.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_test.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_test/modules.html b/_build/large_image_source_test/modules.html new file mode 100644 index 000000000..cee8f339e --- /dev/null +++ b/_build/large_image_source_test/modules.html @@ -0,0 +1,181 @@ + + + + + + + large_image_source_test — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_tiff/large_image_source_tiff.html b/_build/large_image_source_tiff/large_image_source_tiff.html new file mode 100644 index 000000000..477887f75 --- /dev/null +++ b/_build/large_image_source_tiff/large_image_source_tiff.html @@ -0,0 +1,516 @@ + + + + + + + large_image_source_tiff package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_tiff package🔗

+
+

Submodules🔗

+
+
+

large_image_source_tiff.exceptions module🔗

+
+
+exception large_image_source_tiff.exceptions.IOOpenTiffError[source]🔗
+

Bases: IOTiffError

+

An exception caused by an internal failure where the file cannot be opened +by the main library.

+
+ +
+
+exception large_image_source_tiff.exceptions.IOTiffError[source]🔗
+

Bases: TiffError

+

An exception caused by an internal failure, due to an invalid file or other +error.

+
+ +
+
+exception large_image_source_tiff.exceptions.InvalidOperationTiffError[source]🔗
+

Bases: TiffError

+

An exception caused by the user making an invalid request of a TIFF file.

+
+ +
+
+exception large_image_source_tiff.exceptions.TiffError[source]🔗
+

Bases: Exception

+
+ +
+
+exception large_image_source_tiff.exceptions.ValidationTiffError[source]🔗
+

Bases: TiffError

+

An exception caused by the TIFF reader not being able to support a given +file.

+
+ +
+
+

large_image_source_tiff.girder_source module🔗

+
+
+class large_image_source_tiff.girder_source.TiffGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: TiffFileTileSource, GirderTileSource

+

Provides tile access to Girder items with a TIFF file.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'tiff'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

large_image_source_tiff.tiff_reader module🔗

+
+
+class large_image_source_tiff.tiff_reader.TiledTiffDirectory(filePath, directoryNum, mustBeTiled=True, subDirectoryNum=0, validate=True)[source]🔗
+

Bases: object

+

Create a new reader for a tiled image file directory in a TIFF file.

+
+
Parameters:
+
    +
  • filePath (str) – A path to a TIFF file on disk.

  • +
  • directoryNum (int) – The number of the TIFF image file directory to +open.

  • +
  • mustBeTiled (bool) – if True, only tiled images validate. If False, +only non-tiled images validate. None validates both.

  • +
  • subDirectoryNum (int) – if set, the number of the TIFF subdirectory.

  • +
  • validate – if False, don’t validate that images can be read.

  • +
+
+
Raises:
+

InvalidOperationTiffError or IOTiffError or +ValidationTiffError

+
+
+
+
+CoreFunctions = ['SetDirectory', 'SetSubDirectory', 'GetField', 'LastDirectory', 'GetMode', 'IsTiled', 'IsByteSwapped', 'IsUpSampled', 'IsMSB2LSB', 'NumberOfStrips']🔗
+
+ +
+
+getTile(x, y, asarray=False)[source]🔗
+

Get the complete JPEG image from a tile.

+
+
Parameters:
+
    +
  • x (int) – The column index of the desired tile.

  • +
  • y (int) – The row index of the desired tile.

  • +
  • asarray (boolean) – If True, read jpeg compressed images as arrays.

  • +
+
+
Returns:
+

either a buffer with a JPEG or a PIL image.

+
+
Return type:
+

bytes

+
+
Raises:
+

InvalidOperationTiffError or IOTiffError

+
+
+
+ +
+
+property imageHeight🔗
+
+ +
+
+property imageWidth🔗
+
+ +
+
+parse_image_description(meta=None)[source]🔗
+
+ +
+
+property pixelInfo🔗
+
+ +
+
+property tileHeight🔗
+

Get the pixel height of tiles.

+
+
Returns:
+

The tile height in pixels.

+
+
Return type:
+

int

+
+
+
+ +
+
+property tileWidth🔗
+

Get the pixel width of tiles.

+
+
Returns:
+

The tile width in pixels.

+
+
Return type:
+

int

+
+
+
+ +
+ +
+
+large_image_source_tiff.tiff_reader.patchLibtiff()[source]🔗
+
+ +
+
+

Module contents🔗

+
+
+class large_image_source_tiff.TiffFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to TIFF files.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'ptif': SourcePriority.PREFERRED, 'ptiff': SourcePriority.PREFERRED, 'qptiff': SourcePriority.PREFERRED, 'svs': SourcePriority.MEDIUM, 'tif': SourcePriority.HIGH, 'tiff': SourcePriority.HIGH, None: SourcePriority.MEDIUM}🔗
+
+ +
+
+getAssociatedImagesList()[source]🔗
+

Get a list of all associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getTiffDir(directoryNum, mustBeTiled=True, subDirectoryNum=0, validate=True)[source]🔗
+

Get a tile tiff directory reader class.

+
+
Parameters:
+
    +
  • directoryNum – The number of the TIFF image file directory to +open.

  • +
  • mustBeTiled – if True, only tiled images validate. If False, +only non-tiled images validate. None validates both.

  • +
  • subDirectoryNum – if set, the number of the TIFF subdirectory.

  • +
  • validate – if False, don’t validate that images can be read.

  • +
+
+
Returns:
+

a class that can read from a specific tiff directory.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, sparseFallback=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+getTileIOTiffError(x, y, z, pilImageAllowed=False, numpyAllowed=False, sparseFallback=False, exception=None, **kwargs)[source]🔗
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/tiff': SourcePriority.HIGH, 'image/x-ptif': SourcePriority.PREFERRED, 'image/x-tiff': SourcePriority.HIGH, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'tiff'🔗
+
+ +
+ +
+
+large_image_source_tiff.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_tiff.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_tiff/modules.html b/_build/large_image_source_tiff/modules.html new file mode 100644 index 000000000..871e29894 --- /dev/null +++ b/_build/large_image_source_tiff/modules.html @@ -0,0 +1,214 @@ + + + + + + + large_image_source_tiff — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_tifffile/large_image_source_tifffile.html b/_build/large_image_source_tifffile/large_image_source_tifffile.html new file mode 100644 index 000000000..41b0cbfdb --- /dev/null +++ b/_build/large_image_source_tifffile/large_image_source_tifffile.html @@ -0,0 +1,377 @@ + + + + + + + large_image_source_tifffile package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_tifffile package🔗

+
+

Submodules🔗

+
+
+

large_image_source_tifffile.girder_source module🔗

+
+
+class large_image_source_tifffile.girder_source.TifffileGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: TifffileFileTileSource, GirderTileSource

+

Provides tile access to Girder items with files that tifffile can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'tifffile'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_tifffile.TifffileFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to files that the tifffile library can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+classmethod addKnownExtensions()[source]🔗
+
+ +
+
+cacheName = 'tilesource'🔗
+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'scn': SourcePriority.PREFERRED, 'tif': SourcePriority.LOW, 'tiff': SourcePriority.LOW, None: SourcePriority.LOW}🔗
+
+ +
+
+getAssociatedImagesList()[source]🔗
+

Get a list of all associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'image/scn': SourcePriority.PREFERRED, 'image/tiff': SourcePriority.LOW, 'image/x-tiff': SourcePriority.LOW, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+name = 'tifffile'🔗
+
+ +
+ +
+
+large_image_source_tifffile.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+class large_image_source_tifffile.checkForMissingDataHandler(level=0)[source]🔗
+

Bases: Handler

+

Initializes the instance - basically setting the formatter to None +and the filter list to empty.

+
+
+emit(record)[source]🔗
+

Do whatever it takes to actually log the specified logging record.

+

This version is intended to be implemented by subclasses and so +raises a NotImplementedError.

+
+ +
+ +
+
+large_image_source_tifffile.et_findall(tag, text)[source]🔗
+

Find all the child tags in an element tree that end with a specific string.

+
+
Parameters:
+
    +
  • tag – the tag to search.

  • +
  • text – the text to end with.

  • +
+
+
Returns:
+

a list of tags.

+
+
+
+ +
+
+large_image_source_tifffile.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_tifffile/modules.html b/_build/large_image_source_tifffile/modules.html new file mode 100644 index 000000000..c303f4dee --- /dev/null +++ b/_build/large_image_source_tifffile/modules.html @@ -0,0 +1,195 @@ + + + + + + + large_image_source_tifffile — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_vips/large_image_source_vips.html b/_build/large_image_source_vips/large_image_source_vips.html new file mode 100644 index 000000000..d41bc16aa --- /dev/null +++ b/_build/large_image_source_vips/large_image_source_vips.html @@ -0,0 +1,448 @@ + + + + + + + large_image_source_vips package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_vips package🔗

+
+

Submodules🔗

+
+
+

large_image_source_vips.girder_source module🔗

+
+
+class large_image_source_vips.girder_source.VipsGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: VipsFileTileSource, GirderTileSource

+

Vips large_image tile source for Girder.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'vips'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_vips.VipsFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to any libvips compatible file.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+classmethod addKnownExtensions()[source]🔗
+
+ +
+
+addTile(tile, x=0, y=0, mask=None, interpretation=None)[source]🔗
+

Add a numpy or image tile to the image, expanding the image as needed +to accommodate it. Note that x and y can be negative. If so, the +output image (and internal memory access of the image) will act as if +the 0, 0 point is the most negative position. Cropping is applied +after this offset.

+
+
Parameters:
+
    +
  • tile – a numpy array, PIL Image, vips image, or a binary string +with an image. The numpy array can have 2 or 3 dimensions.

  • +
  • x – location in destination for upper-left corner.

  • +
  • y – location in destination for upper-left corner.

  • +
  • mask – a 2-d numpy array (or 3-d if the last dimension is 1). +If specified, areas where the mask is false will not be altered.

  • +
  • interpretation – one of the pyvips.enums.Interpretation or ‘L’, +‘LA’, ‘RGB’, “RGBA’. This defaults to RGB/RGBA for 3/4 channel +images and L/LA for 1/2 channels. The special value ‘pixelmap’ +will convert a 1 channel integer to a 3 channel RGB map. For +images which are not 1 or 3 bands with an optional alpha, specify +MULTIBAND. In this case, the mask option cannot be used.

  • +
+
+
+
+ +
+
+property bandFormat🔗
+
+ +
+
+property bandRanges🔗
+
+ +
+
+cacheName = 'tilesource'🔗
+
+ +
+
+property crop🔗
+

Crop only applies to the output file, not the internal data access.

+

It consists of x, y, w, h in pixels.

+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {None: SourcePriority.LOW}🔗
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getState()[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {None: SourcePriority.FALLBACK}🔗
+
+ +
+
+property minHeight🔗
+
+ +
+
+property minWidth🔗
+
+ +
+
+property mm_x🔗
+
+ +
+
+property mm_y🔗
+
+ +
+
+name = 'vips'🔗
+
+ +
+
+newPriority: SourcePriority | None = 4🔗
+
+ +
+
+property origin🔗
+
+ +
+
+write(path, lossy=True, alpha=True, overwriteAllowed=True, vips_kwargs=None)[source]🔗
+

Output the current image to a file.

+
+
Parameters:
+
    +
  • path – output path.

  • +
  • lossy – if false, emit a lossless file.

  • +
  • alpha – True if an alpha channel is allowed.

  • +
  • overwriteAllowed – if False, raise an exception if the output +path exists.

  • +
  • vips_kwargs – if not None, save the image using these kwargs to +the write_to_file function instead of the automatically chosen +ones. In this case, lossy is ignored and all vips options must be +manually specified.

  • +
+
+
+
+ +
+ +
+
+large_image_source_vips.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_vips.new(*args, **kwargs)[source]🔗
+

Create a new image, collecting the results from patches of numpy arrays or +smaller images.

+
+ +
+
+large_image_source_vips.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_vips/modules.html b/_build/large_image_source_vips/modules.html new file mode 100644 index 000000000..37a048ad8 --- /dev/null +++ b/_build/large_image_source_vips/modules.html @@ -0,0 +1,202 @@ + + + + + + + large_image_source_vips — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_zarr/large_image_source_zarr.html b/_build/large_image_source_zarr/large_image_source_zarr.html new file mode 100644 index 000000000..1616b4572 --- /dev/null +++ b/_build/large_image_source_zarr/large_image_source_zarr.html @@ -0,0 +1,455 @@ + + + + + + + large_image_source_zarr package — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_source_zarr package🔗

+
+

Submodules🔗

+
+
+

large_image_source_zarr.girder_source module🔗

+
+
+class large_image_source_zarr.girder_source.ZarrGirderTileSource(*args, **kwargs)[source]🔗
+

Bases: ZarrFileTileSource, GirderTileSource

+

Provides tile access to Girder items with files that OME Zarr can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+cacheName = 'tilesource'🔗
+
+ +
+
+levels: int🔗
+
+ +
+
+name = 'zarr'🔗
+
+ +
+
+sizeX: int🔗
+
+ +
+
+sizeY: int🔗
+
+ +
+
+tileHeight: int🔗
+
+ +
+
+tileWidth: int🔗
+
+ +
+ +
+
+

Module contents🔗

+
+
+class large_image_source_zarr.ZarrFileTileSource(*args, **kwargs)[source]🔗
+

Bases: FileTileSource

+

Provides tile access to files that the zarr library can read.

+

Initialize the tile class. See the base class for other available +parameters.

+
+
Parameters:
+

path – a filesystem path for the tile source.

+
+
+
+
+addAssociatedImage(image, imageKey=None)[source]🔗
+

Add an associated image to this source.

+
+
Parameters:
+

image – a numpy array, PIL Image, or a binary string +with an image. The numpy array can have 2 or 3 dimensions.

+
+
+
+ +
+
+addTile(tile, x=0, y=0, mask=None, axes=None, **kwargs)[source]🔗
+

Add a numpy or image tile to the image, expanding the image as needed +to accommodate it. Note that x and y can be negative. If so, the +output image (and internal memory access of the image) will act as if +the 0, 0 point is the most negative position. Cropping is applied +after this offset.

+
+
Parameters:
+
    +
  • tile – a numpy array, PIL Image, or a binary string +with an image. The numpy array can have 2 or 3 dimensions.

  • +
  • x – location in destination for upper-left corner.

  • +
  • y – location in destination for upper-left corner.

  • +
  • mask – a 2-d numpy array (or 3-d if the last dimension is 1). +If specified, areas where the mask is false will not be altered.

  • +
  • axes – a string or list of strings specifying the names of axes +in the same order as the tile dimensions.

  • +
  • kwargs – start locations for any additional axes. Note that +level is a reserved word and not permitted for an axis name.

  • +
+
+
+
+ +
+
+cacheName = 'tilesource'🔗
+
+ +
+
+property channelColors🔗
+
+ +
+
+property channelNames🔗
+
+ +
+
+property crop🔗
+

Crop only applies to the output file, not the internal data access.

+

It consists of x, y, w, h in pixels.

+
+ +
+
+extensions: Dict[str | None, SourcePriority] = {'db': SourcePriority.MEDIUM, 'zarr': SourcePriority.PREFERRED, 'zarray': SourcePriority.PREFERRED, 'zattrs': SourcePriority.PREFERRED, 'zgroup': SourcePriority.PREFERRED, 'zip': SourcePriority.LOWER, None: SourcePriority.LOW}🔗
+
+ +
+
+getAssociatedImagesList()[source]🔗
+

Get a list of all associated images.

+
+
Returns:
+

the list of image keys.

+
+
+
+ +
+
+getInternalMetadata(**kwargs)[source]🔗
+

Return additional known metadata about the tile source. Data returned +from this method is not guaranteed to be in any particular format or +have specific values.

+
+
Returns:
+

a dictionary of data or None.

+
+
+
+ +
+
+getMetadata()[source]🔗
+

Return a dictionary of metadata containing levels, sizeX, sizeY, +tileWidth, tileHeight, magnification, mm_x, mm_y, and frames.

+
+
Returns:
+

metadata dictionary.

+
+
+
+ +
+
+getNativeMagnification()[source]🔗
+

Get the magnification at a particular level.

+
+
Returns:
+

magnification, width of a pixel in mm, height of a pixel in mm.

+
+
+
+ +
+
+getState()[source]🔗
+

Return a string reflecting the state of the tile source. This is used +as part of a cache key when hashing function return values.

+
+
Returns:
+

a string hash value of the source state.

+
+
+
+ +
+
+getTile(x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs)[source]🔗
+

Get a tile from a tile source, returning it as an binary image, a PIL +image, or a numpy array.

+
+
Parameters:
+
    +
  • x – the 0-based x position of the tile on the specified z level. +0 is left.

  • +
  • y – the 0-based y position of the tile on the specified z level. +0 is top.

  • +
  • z – the z level of the tile. May range from [0, self.levels], +where 0 is the lowest resolution, single tile for the whole source.

  • +
  • pilImageAllowed – True if a PIL image may be returned.

  • +
  • numpyAllowed – True if a numpy image may be returned. ‘always’ +to return a numpy array.

  • +
  • sparseFallback – if False and a tile doesn’t exist, raise an +error. If True, check if a lower resolution tile exists, and, if +so, interpolate the needed data for this tile.

  • +
  • frame – the frame number within the tile source. None is the +same as 0 for multi-frame sources.

  • +
+
+
Returns:
+

either a numpy array, a PIL image, or a memory object with an +image file.

+
+
+
+ +
+
+property imageDescription🔗
+
+ +
+
+mimeTypes: Dict[str | None, SourcePriority] = {'application/vnd+zarr': SourcePriority.PREFERRED, 'application/x-zarr': SourcePriority.PREFERRED, 'application/zip+zarr': SourcePriority.PREFERRED, None: SourcePriority.FALLBACK}🔗
+
+ +
+
+property mm_x🔗
+
+ +
+
+property mm_y🔗
+
+ +
+
+name = 'zarr'🔗
+
+ +
+
+newPriority: SourcePriority | None = 3🔗
+
+ +
+
+write(path, lossy=True, alpha=True, overwriteAllowed=True, resample=None, **converterParams)[source]🔗
+

Output the current image to a file.

+
+
Parameters:
+
    +
  • path – output path.

  • +
  • lossy – if false, emit a lossless file.

  • +
  • alpha – True if an alpha channel is allowed.

  • +
  • overwriteAllowed – if False, raise an exception if the output +path exists.

  • +
  • resample – one of the ResampleMethod enum values. Defaults +to NP_NEAREST for lossless and non-uint8 data and to +PIL_LANCZOS for lossy uint8 data.

  • +
  • converterParams – options to pass to the large_image_converter if +the output is not a zarr variant.

  • +
+
+
+
+ +
+ +
+
+large_image_source_zarr.canRead(*args, **kwargs)[source]🔗
+

Check if an input can be read by the module class.

+
+ +
+
+large_image_source_zarr.new(*args, **kwargs)[source]🔗
+

Create a new image, collecting the results from patches of numpy arrays or +smaller images.

+
+ +
+
+large_image_source_zarr.open(*args, **kwargs)[source]🔗
+

Create an instance of the module class.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_source_zarr/modules.html b/_build/large_image_source_zarr/modules.html new file mode 100644 index 000000000..301b236f4 --- /dev/null +++ b/_build/large_image_source_zarr/modules.html @@ -0,0 +1,201 @@ + + + + + + + large_image_source_zarr — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_tasks/large_image_tasks.html b/_build/large_image_tasks/large_image_tasks.html new file mode 100644 index 000000000..18e58f4eb --- /dev/null +++ b/_build/large_image_tasks/large_image_tasks.html @@ -0,0 +1,208 @@ + + + + + + + large_image_tasks package — large_image documentation + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

large_image_tasks package🔗

+
+

Submodules🔗

+
+
+

large_image_tasks.tasks module🔗

+
+
+class large_image_tasks.tasks.JobLogger(level=0, job=None, *args, **kwargs)[source]🔗
+

Bases: Handler

+

Initializes the instance - basically setting the formatter to None +and the filter list to empty.

+
+
+emit(record)[source]🔗
+

Do whatever it takes to actually log the specified logging record.

+

This version is intended to be implemented by subclasses and so +raises a NotImplementedError.

+
+ +
+ +
+
+large_image_tasks.tasks.cache_histograms_job(job)[source]🔗
+
+ +
+
+large_image_tasks.tasks.cache_tile_frames_job(job)[source]🔗
+
+ +
+
+large_image_tasks.tasks.convert_image_job(job)[source]🔗
+
+ +
+
+

Module contents🔗

+

Top-level package for Large Image Tasks.

+
+
+class large_image_tasks.LargeImageTasks(app, *args, **kwargs)[source]🔗
+

Bases: GirderWorkerPluginABC

+
+
+task_imports()[source]🔗
+

Plugins should override this method if they have tasks.

+
+ +
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_build/large_image_tasks/modules.html b/_build/large_image_tasks/modules.html new file mode 100644 index 000000000..ac44e84eb --- /dev/null +++ b/_build/large_image_tasks/modules.html @@ -0,0 +1,176 @@ + + + + + + + large_image_tasks — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_images/notebooks_large_image_examples_18_0.jpg b/_images/notebooks_large_image_examples_18_0.jpg new file mode 100644 index 000000000..8c93bd4a7 Binary files /dev/null and b/_images/notebooks_large_image_examples_18_0.jpg differ diff --git a/_images/notebooks_large_image_examples_6_0.jpg b/_images/notebooks_large_image_examples_6_0.jpg new file mode 100644 index 000000000..83c172fe9 Binary files /dev/null and b/_images/notebooks_large_image_examples_6_0.jpg differ diff --git a/_images/notebooks_zarr_sink_example_17_0.jpg b/_images/notebooks_zarr_sink_example_17_0.jpg new file mode 100644 index 000000000..2a510396f Binary files /dev/null and b/_images/notebooks_zarr_sink_example_17_0.jpg differ diff --git a/_images/notebooks_zarr_sink_example_7_0.jpg b/_images/notebooks_zarr_sink_example_7_0.jpg new file mode 100644 index 000000000..ff02b2d7d Binary files /dev/null and b/_images/notebooks_zarr_sink_example_7_0.jpg differ diff --git a/_modules/girder_large_image.html b/_modules/girder_large_image.html new file mode 100644 index 000000000..e88074e83 --- /dev/null +++ b/_modules/girder_large_image.html @@ -0,0 +1,953 @@ + + + + + + girder_large_image — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import datetime
+import json
+import os
+import re
+import threading
+import time
+import warnings
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import yaml
+from girder_jobs.constants import JobStatus
+from girder_jobs.models.job import Job
+
+import girder
+import large_image
+from girder import events, logger
+from girder.api import filter_logging
+from girder.constants import AccessType, SortDir
+from girder.exceptions import RestException, ValidationException
+from girder.models.file import File
+from girder.models.folder import Folder
+from girder.models.group import Group
+from girder.models.item import Item
+from girder.models.notification import Notification
+from girder.models.setting import Setting
+from girder.models.upload import Upload
+from girder.plugin import GirderPlugin, getPlugin
+from girder.settings import SettingDefault
+from girder.utility import config, search, setting_utilities
+from girder.utility.model_importer import ModelImporter
+
+from . import constants, girder_tilesource
+from .girder_tilesource import getGirderTileSource  # noqa
+from .loadmodelcache import invalidateLoadModelCache
+from .models.image_item import ImageItem
+from .rest import addSystemEndpoints
+from .rest.item_meta import InternalMetadataItemResource
+from .rest.large_image_resource import LargeImageResource
+from .rest.tiles import TilesItemResource
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+mimetypes = None
+_configWriteLock = threading.RLock()
+
+
+# Girder 3 is pinned to use pymongo < 4; its warnings aren't relevant until
+# that changes.
+warnings.filterwarnings('ignore', category=UserWarning, module='pymongo')
+
+
+def _postUpload(event):
+    """
+    Called when a file is uploaded. We check the parent item to see if it is
+    expecting a large image upload, and if so we register this file as the
+    result image.
+    """
+    fileObj = event.info['file']
+    # There may not be an itemId (on thumbnails, for instance)
+    if not fileObj.get('itemId'):
+        return
+
+    item = Item().load(fileObj['itemId'], force=True, exc=True)
+
+    if item.get('largeImage', {}).get('expected') and (
+            fileObj['name'].endswith('.tiff') or
+            fileObj.get('mimeType') == 'image/tiff'):
+        if fileObj.get('mimeType') != 'image/tiff':
+            fileObj['mimeType'] = 'image/tiff'
+            File().save(fileObj)
+        del item['largeImage']['expected']
+        item['largeImage']['fileId'] = fileObj['_id']
+        item['largeImage']['sourceName'] = 'tiff'
+        if fileObj['name'].endswith('.geo.tiff'):
+            item['largeImage']['sourceName'] = 'gdal'
+        Item().save(item)
+        # If the job looks finished, update it once more to force notifications
+        if 'jobId' in item['largeImage'] and item['largeImage'].get('notify'):
+            job = Job().load(item['largeImage']['jobId'], force=True)
+            if job and job['status'] == JobStatus.SUCCESS:
+                Job().save(job)
+    else:
+        if 's3FinalizeRequest' in fileObj and 'itemId' in fileObj:
+            logger.info(f'Checking if S3 upload of {fileObj["name"]} is a large image')
+
+            localPath = File().getLocalFilePath(fileObj)
+            for _ in range(300):
+                size = os.path.getsize(localPath)
+                if size == fileObj['size'] and len(
+                        open(localPath, 'rb').read(500)) == min(size, 500):
+                    break
+                logger.info(
+                    f'S3 upload not fully present ({size}/{fileObj["size"]} bytes reported)')
+                time.sleep(0.1)
+            checkForLargeImageFiles(girder.events.Event(event.name, fileObj))
+
+
+def _updateJob(event):
+    """
+    Called when a job is saved, updated, or removed.  If this is a large image
+    job and it is ended, clean up after it.
+    """
+    job = event.info['job'] if event.name == 'jobs.job.update.after' else event.info
+    meta = job.get('meta', {})
+    if (meta.get('creator') != 'large_image' or not meta.get('itemId') or
+            meta.get('task') != 'createImageItem'):
+        return
+    status = job['status']
+    if event.name == 'model.job.remove' and status not in (
+            JobStatus.ERROR, JobStatus.CANCELED, JobStatus.SUCCESS):
+        status = JobStatus.CANCELED
+    if status not in (JobStatus.ERROR, JobStatus.CANCELED, JobStatus.SUCCESS):
+        return
+    item = Item().load(meta['itemId'], force=True)
+    if not item or 'largeImage' not in item:
+        return
+    if item.get('largeImage', {}).get('expected'):
+        # We can get a SUCCESS message before we get the upload message, so
+        # don't clear the expected status on success.
+        if status != JobStatus.SUCCESS:
+            del item['largeImage']['expected']
+        else:
+            return
+    notify = item.get('largeImage', {}).get('notify')
+    msg = None
+    if notify:
+        del item['largeImage']['notify']
+        if status == JobStatus.SUCCESS:
+            msg = 'Large image created'
+        elif status == JobStatus.CANCELED:
+            msg = 'Large image creation canceled'
+        else:  # ERROR
+            msg = 'FAILED: Large image creation failed'
+        msg += ' for item %s' % item['name']
+    if (status in (JobStatus.ERROR, JobStatus.CANCELED) and
+            'largeImage' in item):
+        del item['largeImage']
+    Item().save(item)
+    if msg and event.name != 'model.job.remove':
+        Job().updateJob(job, progressMessage=msg)
+    if notify:
+        Notification().createNotification(
+            type='large_image.finished_image_item',
+            data={
+                'job_id': job['_id'],
+                'item_id': item['_id'],
+                'success': status == JobStatus.SUCCESS,
+                'status': status,
+            },
+            user={'_id': job.get('userId')},
+            expires=(datetime.datetime.now(datetime.timezone.utc) +
+                     datetime.timedelta(seconds=30)))
+
+
+
+[docs] +def checkForLargeImageFiles(event): # noqa + file = event.info + if 'file.save' in event.name and 's3FinalizeRequest' in file: + return + logger.info('Handling file %s (%s)', file['_id'], file['name']) + possible = False + mimeType = file.get('mimeType') + if mimeType in girder_tilesource.KnownMimeTypes: + possible = True + exts = [ext.split()[0] for ext in file.get('exts') if ext] + if set(exts[-2:]).intersection(girder_tilesource.KnownExtensions): + possible = True + if not file.get('itemId'): + return + autoset = Setting().get(constants.PluginSettings.LARGE_IMAGE_AUTO_SET) + if not autoset or (not possible and autoset != 'all'): + return + item = Item().load(file['itemId'], force=True, exc=False) + if not item or item.get('largeImage'): + return + try: + ImageItem().createImageItem(item, file, createJob=False) + return + except Exception: + pass + # Check for files that are from folder/image style images. This is custom + # per folder/image format + imageFolderRecords = { + 'mrxs': { + 'match': r'^Slidedat.ini$', + 'up': 1, + 'folder': r'^(.*)$', + 'image': '\\1.mrxs', + }, + 'vsi': { + 'match': r'^.*\.ets$', + 'up': 2, + 'folder': r'^_(\.*)_$', + 'image': '\\1.vsi', + }, + } + for check in imageFolderRecords.values(): + if re.match(check['match'], file['name']): + try: + folderId = item['folderId'] + folder = None + for _ in range(check['up']): + folder = Folder().load(folderId, force=True) + if not folder: + break + folderId = folder['parentId'] + if not folder or not re.match(check['folder'], folder['name']): + continue + imageName = re.sub(check['folder'], check['image'], folder['name']) + parentItem = Item().findOne({'folderId': folder['parentId'], 'name': imageName}) + if not parentItem: + continue + files = list(Item().childFiles(item=parentItem, limit=2)) + if len(files) == 1: + parentFile = files[0] + ImageItem().createImageItem(parentItem, parentFile, createJob=False) + return + except Exception: + pass + # We couldn't automatically set this as a large image + girder.logger.info( + 'Saved file %s cannot be automatically used as a largeImage' % str(file['_id']))
+ + + +
+[docs] +def removeThumbnails(event): + ImageItem().removeThumbnailFiles(event.info)
+ + + +
+[docs] +def prepareCopyItem(event): + """ + When copying an item, adjust the largeImage fileId reference so it can be + matched to the to-be-copied file. + """ + srcItem, newItem = event.info + if 'largeImage' in newItem: + li = newItem['largeImage'] + for pos, file in enumerate(Item().childFiles(item=srcItem)): + for key in ('fileId', 'originalId'): + if li.get(key) == file['_id']: + li['_index_' + key] = pos + Item().save(newItem, triggerEvents=False)
+ + + +
+[docs] +def handleCopyItem(event): + """ + When copying an item, finish adjusting the largeImage fileId reference to + the copied file. + """ + newItem = event.info + if 'largeImage' in newItem: + li = newItem['largeImage'] + files = list(Item().childFiles(item=newItem)) + for key in ('fileId', 'originalId'): + pos = li.pop('_index_' + key, None) + if pos is not None and 0 <= pos < len(files): + li[key] = files[pos]['_id'] + Item().save(newItem, triggerEvents=False)
+ + + +
+[docs] +def handleRemoveFile(event): + """ + When a file is removed, check if it is a largeImage fileId. If so, delete + the largeImage record. + """ + fileObj = event.info + if fileObj.get('itemId'): + item = Item().load(fileObj['itemId'], force=True, exc=False) + if item and 'largeImage' in item and item['largeImage'].get('fileId') == fileObj['_id']: + ImageItem().delete(item, [fileObj['_id']])
+ + + +
+[docs] +def handleFileSave(event): + """ + When a file is first saved, mark its mime type based on its extension if we + would otherwise just mark it as generic application/octet-stream. + """ + fileObj = event.info + if fileObj.get('mimeType', None) in {None, ''} or ( + '_id' not in fileObj and + fileObj.get('mimeType', None) in {'application/octet-stream'}): + global mimetypes + + if not mimetypes: + import mimetypes + + if not mimetypes.inited: + mimetypes.init() + # Augment the standard mimetypes with some additional values + for mimeType, ext, std in [ + ('text/yaml', '.yaml', True), + ('text/yaml', '.yml', True), + ('application/yaml', '.yaml', True), + ('application/yaml', '.yml', True), + ('application/vnd.geo+json', '.geojson', True), + ]: + if ext not in mimetypes.types_map: + mimetypes.add_type(mimeType, ext, std) + + alt = mimetypes.guess_type(fileObj.get('name', ''))[0] + if alt is not None: + fileObj['mimeType'] = alt
+ + + +
+[docs] +def handleSettingSave(event): + """ + When certain settings are changed, clear the caches. + """ + if event.info.get('key') == constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION: + if event.info['value'] == Setting().get( + constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION): + return + import gc + + from girder.api.rest import setResponseHeader + + large_image.config.setConfig('icc_correction', event.info['value']) + large_image.cache_util.cachesClear() + gc.collect() + try: + # ask the browser to clear the cache; it probably won't be honored + setResponseHeader('Clear-Site-Data', '"cache"') + except Exception: + pass
+ + + +
+[docs] +def metadataSearchHandler( # noqa + query, types, user=None, level=None, limit=0, offset=0, models=None, + searchModels=None, metakey='meta'): + """ + Provide a substring search on metadata. + """ + models = models or {'item', 'folder'} + if any(typ not in models for typ in types): + raise RestException('The metadata search is only able to search in %r.' % models) + if not isinstance(query, str): + msg = 'The search query must be a string.' + raise RestException(msg) + # If we have the beginning of the field specifier, don't do a search + if re.match(r'^(k|ke|key|key:)$', query.strip()): + return {k: [] for k in types} + phrases = re.findall(r'"[^"]*"|\'[^\']*\'|\S+', query) + fields = {phrase.split('key:', 1)[1] for phrase in phrases + if phrase.startswith('key:') and len(phrase.split('key:', 1)[1])} + phrases = [phrase for phrase in phrases + if not phrase.startswith('key:') or not len(phrase.split('key:', 1)[1])] + if not len(fields): + pipeline = [ + {'$project': {'arrayofkeyvalue': {'$objectToArray': '$$ROOT.%s' % metakey}}}, + {'$unwind': '$arrayofkeyvalue'}, + {'$group': {'_id': None, 'allkeys': {'$addToSet': '$arrayofkeyvalue.k'}}}, + ] + for model in (searchModels or types): + modelInst = ModelImporter.model(*model if isinstance(model, tuple) else [model]) + result = list(modelInst.collection.aggregate(pipeline, allowDiskUse=True)) + if len(result): + fields.update(list(result)[0]['allkeys']) + if not len(fields): + return {k: [] for k in types} + logger.debug('Will search the following fields: %r', fields) + usedPhrases = set() + filter = [] + for phrase in phrases: + if phrase[0] == phrase[-1] and phrase[0] in '"\'': + phrase = phrase[1:-1] + if not len(phrase) or phrase in usedPhrases: + continue + usedPhrases.add(phrase) + try: + numval = float(phrase) + delta = abs(float(re.sub(r'[1-9]', '1', re.sub( + r'\d(?=.*[1-9](0*\.|)0*$)', '0', str(numval))))) + except Exception: + numval = None + phrase = re.escape(phrase) + clause = [] + for field in fields: + key = '%s.%s' % (metakey, field) + clause.append({key: {'$regex': phrase, '$options': 'i'}}) + if numval is not None: + clause.append({key: {'$eq': numval}}) + if numval > 0 and delta: + clause.append({key: {'$gte': numval, '$lt': numval + delta}}) + elif numval < 0 and delta: + clause.append({key: {'$lte': numval, '$gt': numval + delta}}) + if len(clause) > 1: + filter.append({'$or': clause}) + else: + filter.append(clause[0]) + if not len(filter): + return [] + filter = {'$and': filter} if len(filter) > 1 else filter[0] + result = {} + logger.debug('Metadata search uses filter: %r' % filter) + for model in searchModels or types: + modelInst = ModelImporter.model(*model if isinstance(model, tuple) else [model]) + if searchModels is None: + result[model] = [ + modelInst.filter(doc, user) + for doc in modelInst.filterResultsByPermission( + modelInst.find(filter), user, level, limit, offset)] + else: + resultModelInst = ModelImporter.model(searchModels[model]['model']) + result[searchModels[model]['model']] = [] + foundIds = set() + for doc in modelInst.filterResultsByPermission(modelInst.find(filter), user, level): + id = doc[searchModels[model]['reference']] + if id in foundIds: + continue + foundIds.add(id) + entry = resultModelInst.load(id=id, user=user, level=level, exc=False) + if entry is not None and offset: + offset -= 1 + continue + elif entry is not None: + result[searchModels[model]['model']].append(resultModelInst.filter(entry, user)) + if limit and len(result[searchModels[model]['model']]) == limit: + break + return result
+ + + +def _mergeDictionaries(a, b): + """ + Merge two dictionaries recursively. If the second dictionary (or any + sub-dictionary) has a special key, value of '__all__': True, the updated + dictionary only contains values from the second dictionary and excludes + the __all__ key. + + :param a: the first dictionary. Modified. + :param b: the second dictionary that gets added to the first. + :returns: the modified first dictionary. + """ + if b.get('__all__') is True: + a.clear() + for key in b: + if isinstance(a.get(key), dict) and isinstance(b[key], dict): + _mergeDictionaries(a[key], b[key]) + elif key != '__all__' or b[key] is not True: + a[key] = b[key] + return a + + +
+[docs] +def adjustConfigForUser(config, user): + """ + Given the current user, adjust the config so that only relevant and + combined values are used. If the root of the config dictionary contains + "access": {"user": <dict>, "admin": <dict>}, the base values are updated + based on the user's access level. If the root of the config contains + "group": {<group-name>: <dict>, ...}, the base values are updated for + every group the user is a part of. + + The order of update is groups in C-sort alphabetical order followed by + access/user and then access/admin as they apply. + + :param config: a config dictionary. + """ + if not isinstance(config, dict): + return config + if isinstance(config.get('groups'), dict): + groups = config.pop('groups') + if user: + for group in Group().find( + {'_id': {'$in': user['groups']}}, sort=[('name', SortDir.ASCENDING)]): + if isinstance(groups.get(group['name']), dict): + config = _mergeDictionaries(config, groups[group['name']]) + if isinstance(config.get('access'), dict): + accessList = config.pop('access') + if user and isinstance(accessList.get('user'), dict): + config = _mergeDictionaries(config, accessList['user']) + if user and user.get('admin') and isinstance(accessList.get('admin'), dict): + config = _mergeDictionaries(config, accessList['admin']) + return config
+ + + +
+[docs] +def yamlConfigFile(folder, name, user): + """ + Get a resolved named config file based on a folder and user. + + :param folder: a Girder folder model. + :param name: the name of the config file. + :param user: the user that the response if adjusted for. + :returns: either None if no config file, or a yaml record. + """ + addConfig = None + last = False + while folder: + item = Item().findOne({'folderId': folder['_id'], 'name': name}) + if item: + for file in Item().childFiles(item): + if file['size'] > 10 * 1024 ** 2: + logger.info('Not loading %s -- too large' % file['name']) + continue + with File().open(file) as fptr: + config = yaml.safe_load(fptr) + if isinstance(config, list) and len(config) == 1: + config = config[0] + # combine and adjust config values based on current user + if isinstance(config, dict) and 'access' in config or 'group' in config: + config = adjustConfigForUser(config, user) + if addConfig and isinstance(config, dict): + config = _mergeDictionaries(config, addConfig) + if not isinstance(config, dict) or config.get('__inherit__') is not True: + return config + config.pop('__inherit__') + addConfig = config + if last: + break + if folder['parentCollection'] != 'folder': + if folder['name'] != '.config': + folder = Folder().findOne({ + 'parentId': folder['parentId'], + 'parentCollection': folder['parentCollection'], + 'name': '.config'}) + else: + last = 'setting' + if not folder or last == 'setting': + folderId = Setting().get(constants.PluginSettings.LARGE_IMAGE_CONFIG_FOLDER) + if not folderId: + break + folder = Folder().load(folderId, force=True) + last = True + else: + folder = Folder().load(folder['parentId'], user=user, level=AccessType.READ) + return addConfig
+ + + +
+[docs] +def yamlConfigFileWrite(folder, name, user, yaml_config): + """ + If the user has appropriate permissions, create or modify an item in the + specified folder with the specified name, storing the config value as a + file. + + :param folder: a Girder folder model. + :param name: the name of the config file. + :param user: the user that the response if adjusted for. + :param yaml_config: a yaml config string. + """ + # Check that we have valid yaml + yaml.safe_load(yaml_config) + item = Item().createItem(name, user, folder, reuseExisting=True) + existingFiles = list(Item().childFiles(item)) + if (len(existingFiles) == 1 and + existingFiles[0]['mimeType'] == 'application/yaml' and + existingFiles[0]['name'] == name): + upload = Upload().createUploadToFile( + existingFiles[0], user, size=len(yaml_config)) + else: + upload = Upload().createUpload( + user, name, 'item', item, size=len(yaml_config), + mimeType='application/yaml', save=True) + newfile = Upload().handleChunk(upload, yaml_config) + with _configWriteLock: + for entry in list(Item().childFiles(item)): + if entry['_id'] != newfile['_id'] and len(Item().childFiles(item)) > 1: + File().remove(entry)
+ + + +# Validators + +
+[docs] +@setting_utilities.validator({ + constants.PluginSettings.LARGE_IMAGE_SHOW_THUMBNAILS, + constants.PluginSettings.LARGE_IMAGE_SHOW_VIEWER, + constants.PluginSettings.LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK, +}) +def validateBoolean(doc): + val = doc['value'] + if str(val).lower() not in ('false', 'true', ''): + raise ValidationException('%s must be a boolean.' % doc['key'], 'value') + doc['value'] = (str(val).lower() != 'false')
+ + + +
+[docs] +@setting_utilities.validator({ + constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION, +}) +def validateBooleanOrICCIntent(doc): + import PIL.ImageCms + + val = doc['value'] + if ((hasattr(PIL.ImageCms, 'Intent') and hasattr(PIL.ImageCms.Intent, str(val).upper())) or + hasattr(PIL.ImageCms, 'INTENT_' + str(val).upper())): + doc['value'] = str(val).upper() + else: + if str(val).lower() not in ('false', 'true', ''): + raise ValidationException( + '%s must be a boolean or a named intent.' % doc['key'], 'value') + doc['value'] = (str(val).lower() != 'false')
+ + + +
+[docs] +@setting_utilities.validator({ + constants.PluginSettings.LARGE_IMAGE_AUTO_SET, +}) +def validateBooleanOrAll(doc): + val = doc['value'] + if str(val).lower() not in ('false', 'true', 'all', ''): + raise ValidationException('%s must be a boolean or "all".' % doc['key'], 'value') + doc['value'] = val if val in {'all'} else (str(val).lower() != 'false')
+ + + +
+[docs] +@setting_utilities.validator({ + constants.PluginSettings.LARGE_IMAGE_SHOW_EXTRA_PUBLIC, + constants.PluginSettings.LARGE_IMAGE_SHOW_EXTRA, + constants.PluginSettings.LARGE_IMAGE_SHOW_EXTRA_ADMIN, + constants.PluginSettings.LARGE_IMAGE_SHOW_ITEM_EXTRA_PUBLIC, + constants.PluginSettings.LARGE_IMAGE_SHOW_ITEM_EXTRA, + constants.PluginSettings.LARGE_IMAGE_SHOW_ITEM_EXTRA_ADMIN, +}) +def validateDictOrJSON(doc): + val = doc['value'] + try: + if isinstance(val, dict): + doc['value'] = json.dumps(val) + elif val is None or val.strip() == '': + doc['value'] = '' + else: + parsed = json.loads(val) + if not isinstance(parsed, dict): + raise ValidationException('%s must be a JSON object.' % doc['key'], 'value') + doc['value'] = val.strip() + except (ValueError, AttributeError): + raise ValidationException('%s must be a JSON object.' % doc['key'], 'value')
+ + + +
+[docs] +@setting_utilities.validator({ + constants.PluginSettings.LARGE_IMAGE_MAX_THUMBNAIL_FILES, + constants.PluginSettings.LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE, +}) +def validateNonnegativeInteger(doc): + val = doc['value'] + try: + val = int(val) + if val < 0: + raise ValueError + except ValueError: + raise ValidationException('%s must be a non-negative integer.' % ( + doc['key'], ), 'value') + doc['value'] = val
+ + + +
+[docs] +@setting_utilities.validator({ + constants.PluginSettings.LARGE_IMAGE_DEFAULT_VIEWER, +}) +def validateDefaultViewer(doc): + doc['value'] = str(doc['value']).strip()
+ + + +
+[docs] +@setting_utilities.validator(constants.PluginSettings.LARGE_IMAGE_CONFIG_FOLDER) +def validateFolder(doc): + if not doc.get('value', None): + doc['value'] = None + else: + Folder().load(doc['value'], force=True, exc=True)
+ + + +# Defaults + +# Defaults that have fixed values can just be added to the system defaults +# dictionary. +SettingDefault.defaults.update({ + constants.PluginSettings.LARGE_IMAGE_SHOW_THUMBNAILS: True, + constants.PluginSettings.LARGE_IMAGE_SHOW_VIEWER: True, + constants.PluginSettings.LARGE_IMAGE_AUTO_SET: True, + constants.PluginSettings.LARGE_IMAGE_MAX_THUMBNAIL_FILES: 10, + constants.PluginSettings.LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE: 4096, + constants.PluginSettings.LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK: True, + constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION: True, +}) + + +
+[docs] +def unbindGirderEventsByHandlerName(handlerName): + for eventName in events._mapping: + events.unbind(eventName, handlerName)
+ + + +
+[docs] +def patchMount(): + try: + import girder.cli.mount + + def _flatItemFile(self, item): + return next(File().collection.aggregate([ + {'$match': {'itemId': item['_id']}}, + ] + ([ + {'$addFields': {'matchLI': {'$eq': ['$_id', item['largeImage']['fileId']]}}}, + ] if 'largeImage' in item and 'expected' not in item['largeImage'] else []) + [ + {'$addFields': {'matchName': {'$eq': ['$name', item['name']]}}}, + {'$sort': {'matchLI': -1, 'matchName': -1, '_id': 1}}, + {'$limit': 1}, + ]), None) + + girder.cli.mount.ServerFuse._flatItemFile = _flatItemFile + except Exception: + pass
+ + + +
+[docs] +class LargeImagePlugin(GirderPlugin): + DISPLAY_NAME = 'Large Image' + CLIENT_SOURCE_PATH = 'web_client' + +
+[docs] + def load(self, info): + try: + getPlugin('worker').load(info) + except Exception: + logger.debug('worker plugin is unavailable') + + unbindGirderEventsByHandlerName('large_image') + + ModelImporter.registerModel('image_item', ImageItem, 'large_image') + large_image.config.setConfig('logger', girder.logger) + large_image.config.setConfig('logprint', girder.logprint) + # Load girder's large_image config + curConfig = config.getConfig().get('large_image') + for key, value in (curConfig or {}).items(): + large_image.config.setConfig(key, value) + large_image.config.setConfig('icc_correction', Setting().get( + constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION)) + addSystemEndpoints(info['apiRoot']) + + girder_tilesource.loadGirderTileSources() + TilesItemResource(info['apiRoot']) + InternalMetadataItemResource(info['apiRoot']) + info['apiRoot'].large_image = LargeImageResource() + + Item().exposeFields(level=AccessType.READ, fields='largeImage') + + events.bind('data.process', 'large_image', _postUpload) + events.bind('jobs.job.update.after', 'large_image', _updateJob) + events.bind('model.job.save', 'large_image', _updateJob) + events.bind('model.job.remove', 'large_image', _updateJob) + events.bind('model.folder.save.after', 'large_image', invalidateLoadModelCache) + events.bind('model.group.save.after', 'large_image', invalidateLoadModelCache) + events.bind('model.user.save.after', 'large_image', invalidateLoadModelCache) + events.bind('model.collection.save.after', 'large_image', invalidateLoadModelCache) + events.bind('model.item.remove', 'large_image', invalidateLoadModelCache) + events.bind('model.item.copy.prepare', 'large_image', prepareCopyItem) + events.bind('model.item.copy.after', 'large_image', handleCopyItem) + events.bind('model.item.save.after', 'large_image', invalidateLoadModelCache) + events.bind('model.file.save.after', 'large_image', checkForLargeImageFiles) + filter_logging.addLoggingFilter( + r'Handling file ([0-9a-f]{24}) \(', + frequency=1000, duration=10) + events.bind('model.item.remove', 'large_image.removeThumbnails', removeThumbnails) + events.bind('server_fuse.unmount', 'large_image', large_image.cache_util.cachesClear) + events.bind('model.file.remove', 'large_image', handleRemoveFile) + events.bind('model.file.save', 'large_image', handleFileSave) + events.bind('model.setting.save', 'large_image', handleSettingSave) + + search._allowedSearchMode.pop('li_metadata', None) + search.addSearchMode('li_metadata', metadataSearchHandler) + + patchMount()
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image/constants.html b/_modules/girder_large_image/constants.html new file mode 100644 index 000000000..7635b3309 --- /dev/null +++ b/_modules/girder_large_image/constants.html @@ -0,0 +1,157 @@ + + + + + + girder_large_image.constants — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image.constants

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+
+# Constants representing the setting keys for this plugin
+
+[docs] +class PluginSettings: + LARGE_IMAGE_AUTO_SET = 'large_image.auto_set' + LARGE_IMAGE_AUTO_USE_ALL_FILES = 'large_image.auto_use_all_files' + LARGE_IMAGE_CONFIG_FOLDER = 'large_image.config_folder' + LARGE_IMAGE_DEFAULT_VIEWER = 'large_image.default_viewer' + LARGE_IMAGE_ICC_CORRECTION = 'large_image.icc_correction' + LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE = 'large_image.max_small_image_size' + LARGE_IMAGE_MAX_THUMBNAIL_FILES = 'large_image.max_thumbnail_files' + LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK = 'large_image.notification_stream_fallback' + LARGE_IMAGE_SHOW_EXTRA = 'large_image.show_extra' + LARGE_IMAGE_SHOW_EXTRA_ADMIN = 'large_image.show_extra_admin' + LARGE_IMAGE_SHOW_EXTRA_PUBLIC = 'large_image.show_extra_public' + LARGE_IMAGE_SHOW_ITEM_EXTRA = 'large_image.show_item_extra' + LARGE_IMAGE_SHOW_ITEM_EXTRA_ADMIN = 'large_image.show_item_extra_admin' + LARGE_IMAGE_SHOW_ITEM_EXTRA_PUBLIC = 'large_image.show_item_extra_public' + LARGE_IMAGE_SHOW_THUMBNAILS = 'large_image.show_thumbnails' + LARGE_IMAGE_SHOW_VIEWER = 'large_image.show_viewer'
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image/girder_tilesource.html b/_modules/girder_large_image/girder_tilesource.html new file mode 100644 index 000000000..7520cbdb8 --- /dev/null +++ b/_modules/girder_large_image/girder_tilesource.html @@ -0,0 +1,357 @@ + + + + + + girder_large_image.girder_tilesource — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image.girder_tilesource

+import inspect
+import os
+import re
+
+from girder.constants import AccessType
+from girder.exceptions import FilePathException, ValidationException
+from girder.models.file import File
+from girder.models.item import Item
+from large_image import config, tilesource
+from large_image.constants import SourcePriority
+from large_image.exceptions import TileSourceAssetstoreError, TileSourceError
+
+AvailableGirderTileSources = {}
+KnownMimeTypes = set()
+KnownExtensions = set()
+KnownMimeTypesWithAdjacentFiles = set()
+KnownExtensionsWithAdjacentFiles = set()
+
+
+
+[docs] +class GirderTileSource(tilesource.FileTileSource): + girderSource = True + + # If the large image file has one of these extensions or mimetypes, it will + # always be treated as if there are possible adjacent files. + extensionsWithAdjacentFiles = set() + mimeTypesWithAdjacentFiles = set() + + def __init__(self, item, *args, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param item: a Girder item document which contains + ['largeImage']['fileId'] identifying the Girder file to be used + for the tile source. + """ + super().__init__(item, *args, **kwargs) + self.item = item + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs): + return '%s,%s,%s,%s,%s,%s,%s,__STYLESTART__,%s,__STYLEEND__' % ( + args[0]['largeImage']['fileId'], + args[0]['updated'], + kwargs.get('encoding', 'JPEG'), + kwargs.get('jpegQuality', 95), + kwargs.get('jpegSubsampling', 0), + kwargs.get('tiffCompression', 'raw'), + kwargs.get('edge', False), + kwargs.get('style', None))
+ + +
+[docs] + def getState(self): + if hasattr(self, '_classkey'): + return self._classkey + return '%s,%s,%s,%s,%s,%s,%s,__STYLESTART__,%s,__STYLEEND__' % ( + self.item['largeImage']['fileId'], + self.item['updated'], + self.encoding, + self.jpegQuality, + self.jpegSubsampling, + self.tiffCompression, + self.edge, + self._jsonstyle)
+ + +
+[docs] + def mayHaveAdjacentFiles(self, largeImageFile): + if largeImageFile.get('linkUrl'): + return True + if not hasattr(self, '_mayHaveAdjacentFiles'): + largeImageFileId = self.item['largeImage']['fileId'] + # The item has adjacent files if there are any files that are not + # the large image file or an original file it was derived from. + # This is always the case if there are 3 or more files. + fileIds = [str(file['_id']) for file in Item().childFiles(self.item, limit=3)] + knownIds = [str(largeImageFileId)] + if 'originalId' in self.item['largeImage']: + knownIds.append(str(self.item['largeImage']['originalId'])) + self._mayHaveAdjacentFiles = ( + len(fileIds) >= 3 or + fileIds[0] not in knownIds or + fileIds[-1] not in knownIds) + if (any(ext in KnownExtensionsWithAdjacentFiles for ext in largeImageFile['exts']) or + largeImageFile.get('mimeType') in KnownMimeTypesWithAdjacentFiles): + self._mayHaveAdjacentFiles = True + return self._mayHaveAdjacentFiles
+ + + def _getLargeImagePath(self): + # If self.mayHaveAdjacentFiles is True, we try to use the girder + # mount where companion files appear next to each other. + largeImageFileId = self.item['largeImage']['fileId'] + largeImageFile = File().load(largeImageFileId, force=True) + try: + largeImagePath = None + if (self.mayHaveAdjacentFiles(largeImageFile) and + hasattr(File(), 'getGirderMountFilePath')): + try: + if (largeImageFile.get('imported') and + File().getLocalFilePath(largeImageFile) == largeImageFile['path']): + largeImagePath = largeImageFile['path'] + except Exception: + pass + if not largeImagePath: + try: + largeImagePath = File().getGirderMountFilePath( + largeImageFile, + **({'preferFlat': True} if 'preferFlat' in inspect.signature( + File.getGirderMountFilePath).parameters else {})) + except FilePathException: + pass + if not largeImagePath: + try: + largeImagePath = File().getLocalFilePath(largeImageFile) + except AttributeError as e: + raise TileSourceError( + 'No local file path for this file: %s' % e.args[0]) + return largeImagePath + except (TileSourceAssetstoreError, FilePathException): + raise + except (KeyError, ValidationException, TileSourceError) as e: + raise TileSourceError( + 'No large image file in this item: %s' % e.args[0])
+ + + +
+[docs] +def loadGirderTileSources(): + """ + Load all Girder tilesources from entrypoints and add them to the + AvailableGiderTileSources dictionary. + """ + tilesource.loadTileSources('girder_large_image.source', AvailableGirderTileSources) + for sourceName in AvailableGirderTileSources: + if getattr(AvailableGirderTileSources[sourceName], 'girderSource', False): + KnownExtensions.update({ + key for key in AvailableGirderTileSources[sourceName].extensions + if key is not None}) + KnownExtensionsWithAdjacentFiles.update({ + key for key in AvailableGirderTileSources[sourceName].extensionsWithAdjacentFiles + if key is not None}) + if getattr(AvailableGirderTileSources[sourceName], 'girderSource', False): + KnownMimeTypes.update({ + key for key in AvailableGirderTileSources[sourceName].mimeTypes + if key is not None}) + KnownMimeTypesWithAdjacentFiles.update({ + key for key in AvailableGirderTileSources[sourceName].mimeTypesWithAdjacentFiles + if key is not None})
+ + + +
+[docs] +def getGirderTileSourceName(item, file=None, *args, **kwargs): # noqa + """ + Get a Girder tilesource name using the known sources. If tile sources have + not yet been loaded, load them. + + :param item: a Girder item. + :param file: if specified, the Girder file object to use as the large image + file; used here only to check extensions. + :returns: The name of a tilesource that can read the Girder item. + """ + if not len(AvailableGirderTileSources): + loadGirderTileSources() + availableSources = AvailableGirderTileSources + if not file: + file = File().load(item['largeImage']['fileId'], force=True) + mimeType = file['mimeType'] + try: + localPath = File().getLocalFilePath(file) + except (FilePathException, AttributeError): + localPath = None + extensions = [entry.lower().split()[0] for entry in file['exts'] if entry] + baseName = os.path.basename(file['name']) + properties = {} + if localPath: + properties['_geospatial_source'] = tilesource.isGeospatial(localPath) + ignored_names = config.getConfig('all_sources_ignored_names') + ignoreName = (ignored_names and re.search( + ignored_names, baseName, flags=re.IGNORECASE)) + sourceList = [] + for sourceName in availableSources: + if not getattr(availableSources[sourceName], 'girderSource', False): + continue + sourceExtensions = availableSources[sourceName].extensions + priority = sourceExtensions.get(None, SourcePriority.MANUAL) + fallback = True + if (mimeType and getattr(availableSources[sourceName], 'mimeTypes', None) and + mimeType in availableSources[sourceName].mimeTypes): + priority = min(priority, availableSources[sourceName].mimeTypes[mimeType]) + fallback = False + for regex in getattr(availableSources[sourceName], 'nameMatches', {}): + if re.match(regex, baseName): + priority = min(priority, availableSources[sourceName].nameMatches[regex]) + fallback = False + for ext in extensions: + if ext in sourceExtensions: + priority = min(priority, sourceExtensions[ext]) + fallback = False + if priority >= SourcePriority.MANUAL or (ignoreName and fallback): + continue + propertiesClash = any( + getattr(availableSources[sourceName], k, False) != v + for k, v in properties.items()) + sourceList.append((propertiesClash, fallback, priority, sourceName)) + for _clash, _fallback, _priority, sourceName in sorted(sourceList): + if availableSources[sourceName].canRead(item, *args, **kwargs): + return sourceName
+ + + +
+[docs] +def getGirderTileSource(item, file=None, *args, **kwargs): + """ + Get a Girder tilesource using the known sources. + + :param item: a Girder item or an item id. + :param file: if specified, the Girder file object to use as the large image + file; used here only to check extensions. + :returns: A girder tilesource for the item. + """ + if not isinstance(item, dict): + item = Item().load(item, user=kwargs.get('user', None), level=AccessType.READ) + sourceName = getGirderTileSourceName(item, file, *args, **kwargs) + if sourceName: + return AvailableGirderTileSources[sourceName](item, *args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image/loadmodelcache.html b/_modules/girder_large_image/loadmodelcache.html new file mode 100644 index 000000000..67d1a8120 --- /dev/null +++ b/_modules/girder_large_image/loadmodelcache.html @@ -0,0 +1,209 @@ + + + + + + girder_large_image.loadmodelcache — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image.loadmodelcache

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import time
+
+import cherrypy
+
+from girder.api.rest import getCurrentToken
+from girder.utility.model_importer import ModelImporter
+
+LoadModelCache = {}
+LoadModelCacheMaxEntries = 100
+LoadModelCacheExpiryDuration = 300  # seconds
+
+
+
+[docs] +def invalidateLoadModelCache(*args, **kwargs): + """ + Empty the LoadModelCache. + """ + LoadModelCache.clear()
+ + + +
+[docs] +def loadModel(resource, model, plugin='_core', id=None, allowCookie=False, + level=None): + """ + Load a model based on id using the current cherrypy token parameter for + authentication, caching the results. This must be called in a cherrypy + context. + + :param resource: the resource class instance calling the function. Used + for access to the current user and model importer. + :param model: the model name, e.g., 'item'. + :param plugin: the plugin name when loading a plugin model. + :param id: a string id of the model to load. + :param allowCookie: true if the cookie authentication method is allowed. + :param level: access level desired. + :returns: the loaded model. + """ + key = tokenStr = None + if 'token' in cherrypy.request.params: # Token as a parameter + tokenStr = cherrypy.request.params.get('token') + elif 'Girder-Token' in cherrypy.request.headers: + tokenStr = cherrypy.request.headers['Girder-Token'] + elif 'girderToken' in cherrypy.request.cookie and allowCookie: + tokenStr = cherrypy.request.cookie['girderToken'].value + key = (model, tokenStr, id) + cacheEntry = LoadModelCache.get(key) + if cacheEntry and cacheEntry['expiry'] > time.time(): + entry = cacheEntry['result'] + cacheEntry['hits'] += 1 + else: + # we have to get the token separately from the user if we are using + # cookies. + if allowCookie: + getCurrentToken(allowCookie) + cherrypy.request.girderAllowCookie = True + entry = ModelImporter.model(model, plugin).load( + id=id, level=level, user=resource.getCurrentUser()) + # If the cache becomes too large, just dump it -- this is simpler + # than dropping the oldest values and avoids having to add locking. + if len(LoadModelCache) > LoadModelCacheMaxEntries: + LoadModelCache.clear() + LoadModelCache[key] = { + 'id': id, + 'model': model, + 'tokenId': tokenStr, + 'expiry': time.time() + LoadModelCacheExpiryDuration, + 'result': entry, + 'hits': 0, + } + return entry
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image/models/image_item.html b/_modules/girder_large_image/models/image_item.html new file mode 100644 index 000000000..91b749d80 --- /dev/null +++ b/_modules/girder_large_image/models/image_item.html @@ -0,0 +1,873 @@ + + + + + + girder_large_image.models.image_item — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image.models.image_item

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import io
+import json
+import pickle
+import threading
+
+import pymongo
+from girder_jobs.constants import JobStatus
+from girder_jobs.models.job import Job
+
+from girder import logger
+from girder.constants import SortDir
+from girder.exceptions import FilePathException, GirderException, ValidationException
+from girder.models.assetstore import Assetstore
+from girder.models.file import File
+from girder.models.item import Item
+from girder.models.setting import Setting
+from girder.models.upload import Upload
+from large_image.cache_util import getTileCache, strhash
+from large_image.constants import TileOutputMimeTypes
+from large_image.exceptions import TileGeneralError, TileSourceError
+
+from .. import constants, girder_tilesource
+
+
+
+[docs] +class ImageItem(Item): + # We try these sources in this order. The first entry is the fallback for + # items that antedate there being multiple options. +
+[docs] + def initialize(self): + super().initialize() + self.ensureIndices(['largeImage.fileId']) + File().ensureIndices([ + ([ + ('isLargeImageThumbnail', pymongo.ASCENDING), + ('attachedToType', pymongo.ASCENDING), + ('attachedToId', pymongo.ASCENDING), + ], {}), + ([ + ('isLargeImageData', pymongo.ASCENDING), + ('attachedToType', pymongo.ASCENDING), + ('attachedToId', pymongo.ASCENDING), + ], {}), + ])
+ + +
+[docs] + def createImageItem(self, item, fileObj, user=None, token=None, + createJob=True, notify=False, localJob=None, **kwargs): + logger.info('createImageItem called on item %s (%s)', item['_id'], item['name']) + # Using setdefault ensures that 'largeImage' is in the item + if 'fileId' in item.setdefault('largeImage', {}): + msg = 'Item already has largeImage set.' + raise TileGeneralError(msg) + if fileObj['itemId'] != item['_id']: + msg = 'The provided file must be in the provided item.' + raise TileGeneralError(msg) + if (item['largeImage'].get('expected') is True and + 'jobId' in item['largeImage']): + msg = 'Item is scheduled to generate a largeImage.' + raise TileGeneralError(msg) + + item['largeImage'].pop('expected', None) + item['largeImage'].pop('sourceName', None) + + item['largeImage']['fileId'] = fileObj['_id'] + job = None + logger.debug( + 'createImageItem checking if item %s (%s) can be used directly', + item['_id'], item['name']) + sourceName = girder_tilesource.getGirderTileSourceName(item, fileObj, noCache=True) + if sourceName: + logger.info( + 'createImageItem using source %s for item %s (%s)', + sourceName, item['_id'], item['name']) + item['largeImage']['sourceName'] = sourceName + if not sourceName or createJob == 'always': + if not createJob: + logger.info( + 'createImageItem will not use item %s (%s) as a largeImage', + item['_id'], item['name']) + msg = 'A job must be used to generate a largeImage.' + raise TileGeneralError(msg) + logger.debug( + 'createImageItem creating a job to generate a largeImage for item %s (%s)', + item['_id'], item['name']) + # No source was successful + del item['largeImage']['fileId'] + if not localJob: + job = self._createLargeImageJob(item, fileObj, user, token, **kwargs) + else: + job = self._createLargeImageLocalJob(item, fileObj, user, **kwargs) + item['largeImage']['expected'] = True + item['largeImage']['notify'] = notify + item['largeImage']['originalId'] = fileObj['_id'] + item['largeImage']['jobId'] = job['_id'] + logger.debug( + 'createImageItem created a job to generate a largeImage for item %s (%s)', + item['_id'], item['name']) + self.save(item) + return job
+ + + def _createLargeImageJob( + self, item, fileObj, user, token, toFolder=False, folderId=None, name=None, **kwargs): + import large_image_tasks.tasks + from girder_worker_utils.transforms.common import TemporaryDirectory + from girder_worker_utils.transforms.contrib.girder_io import GirderFileIdAllowDirect + from girder_worker_utils.transforms.girder_io import (GirderUploadToFolder, + GirderUploadToItem) + + try: + localPath = File().getLocalFilePath(fileObj) + except (FilePathException, AttributeError): + localPath = None + job = large_image_tasks.tasks.create_tiff.apply_async(kwargs=dict( + girder_job_title='Large Image Conversion: %s' % fileObj['name'], + girder_job_other_fields={'meta': { + 'creator': 'large_image', + 'itemId': str(item['_id']), + 'task': 'createImageItem', + }}, + inputFile=GirderFileIdAllowDirect(str(fileObj['_id']), fileObj['name'], localPath), + inputName=fileObj['name'], + outputDir=TemporaryDirectory(), + girder_result_hooks=[ + GirderUploadToItem(str(item['_id']), False) + if not toFolder else + GirderUploadToFolder( + str(folderId or item['folderId']), + upload_kwargs=dict(filename=name), + ), + ], + **kwargs, + ), countdown=int(kwargs['countdown']) if kwargs.get('countdown') else None) + return job.job + + def _createLargeImageLocalJob( + self, item, fileObj, user=None, toFolder=False, folderId=None, name=None, **kwargs): + job = Job().createLocalJob( + module='large_image_tasks.tasks', + function='convert_image_job', + kwargs={ + 'itemId': str(item['_id']), + 'fileId': str(fileObj['_id']), + 'userId': str(user['_id']) if user else None, + 'toFolder': toFolder, + **kwargs, + }, + title='Large Image Conversion: %s' % fileObj['name'], + type='large_image_tiff', + user=user, + public=True, + asynchronous=True, + ) + # For consistency with the non-local job + job['meta'] = { + 'creator': 'large_image', + 'itemId': str(item['_id']), + 'task': 'createImageItem', + } + job = Job().save(job) + Job().scheduleJob(job) + return job + +
+[docs] + def convertImage(self, item, fileObj, user=None, token=None, localJob=True, **kwargs): + if fileObj['itemId'] != item['_id']: + msg = 'The provided file must be in the provided item.' + raise TileGeneralError(msg) + if not localJob: + return self._createLargeImageJob(item, fileObj, user, token, toFolder=True, **kwargs) + return self._createLargeImageLocalJob(item, fileObj, user, toFolder=True, **kwargs)
+ + + @classmethod + def _tileFromHash(cls, item, x, y, z, mayRedirect=False, **kwargs): + tileCache, tileCacheLock = getTileCache() + if tileCache is None: + return None + if 'largeImage' not in item: + return None + if item['largeImage'].get('expected'): + return None + sourceName = item['largeImage']['sourceName'] + try: + sourceClass = girder_tilesource.AvailableGirderTileSources[sourceName] + except TileSourceError: + return None + if '_' in kwargs or 'style' in kwargs: + kwargs = kwargs.copy() + kwargs.pop('_', None) + classHash = sourceClass.getLRUHash(item, **kwargs) + # style isn't part of the tile hash strhash parameters + kwargs.pop('style', None) + tileHash = sourceClass.__name__ + ' ' + classHash + ' ' + strhash( + sourceClass.__name__ + ' ' + classHash) + strhash( + *(x, y, z), mayRedirect=mayRedirect, **kwargs) + try: + if tileCacheLock is None: + tileData = tileCache[tileHash] + else: + # It would be nice if we could test if tileHash was in + # tileCache, but memcached doesn't expose that functionality + with tileCacheLock: + tileData = tileCache[tileHash] + tileMime = TileOutputMimeTypes.get(kwargs.get('encoding'), 'image/jpeg') + return tileData, tileMime + except (KeyError, ValueError): + return None + + @classmethod + def _loadTileSource(cls, item, **kwargs): + if 'largeImage' not in item: + msg = 'No large image file in this item.' + raise TileSourceError(msg) + if item['largeImage'].get('expected'): + msg = 'The large image file for this item is still pending creation.' + raise TileSourceError(msg) + + sourceName = item['largeImage']['sourceName'] + try: + # First try to use the tilesource we recorded as the preferred one. + # This is faster than trying to find the best source each time. + tileSource = girder_tilesource.AvailableGirderTileSources[sourceName](item, **kwargs) + except TileSourceError as exc: + # We could try any source + # tileSource = girder_tilesource.getGirderTileSource(item, **kwargs) + # but, instead, log that the original source no longer works are + # reraise the exception + logger.warning('The original tile source for item %s is not working' % item['_id']) + try: + file = File().load(item['largeImage']['fileId'], force=True) + localPath = File().getLocalFilePath(file) + open(localPath, 'rb').read(1) + except OSError: + logger.warning( + 'Is the original data reachable and readable (it fails via %r)?', localPath) + raise OSError(localPath) from None + except Exception: + pass + raise exc + return tileSource + +
+[docs] + def getMetadata(self, item, **kwargs): + tileSource = self._loadTileSource(item, **kwargs) + return tileSource.getMetadata()
+ + +
+[docs] + def getInternalMetadata(self, item, **kwargs): + tileSource = self._loadTileSource(item, **kwargs) + result = tileSource.getInternalMetadata() or {} + if tileSource.getICCProfiles(onlyInfo=True): + result['iccprofiles'] = tileSource.getICCProfiles(onlyInfo=True) + result['tilesource'] = tileSource.name + if hasattr(tileSource, '_populatedLevels'): + result['populatedLevels'] = tileSource._populatedLevels + return result
+ + +
+[docs] + def getTile(self, item, x, y, z, mayRedirect=False, **kwargs): + tileSource = self._loadTileSource(item, **kwargs) + imageParams = {} + if 'frame' in kwargs: + imageParams['frame'] = int(kwargs['frame']) + tileData = tileSource.getTile(x, y, z, mayRedirect=mayRedirect, **imageParams) + tileMimeType = tileSource.getTileMimeType() + return tileData, tileMimeType
+ + +
+[docs] + def delete(self, item, skipFileIds=None): + deleted = False + if 'largeImage' in item: + job = None + if 'jobId' in item['largeImage']: + try: + job = Job().load(item['largeImage']['jobId'], force=True, exc=True) + except ValidationException: + # The job has been deleted, but we still need to clean up + # the rest of the tile information + pass + if (item['largeImage'].get('expected') and job and + job.get('status') in ( + JobStatus.QUEUED, JobStatus.RUNNING)): + # cannot cleanly remove the large image, since a conversion + # job is currently in progress + # TODO: cancel the job + # TODO: return a failure error code + return False + + # If this file was created by the worker job, delete it + if 'jobId' in item['largeImage']: + # To eliminate all traces of the job, add + # if job: + # Job().remove(job) + del item['largeImage']['jobId'] + + if 'originalId' in item['largeImage']: + # The large image file should not be the original file + assert item['largeImage']['originalId'] != \ + item['largeImage'].get('fileId') + + if ('fileId' in item['largeImage'] and ( + not skipFileIds or + item['largeImage']['fileId'] not in skipFileIds)): + file = File().load(id=item['largeImage']['fileId'], force=True) + if file: + File().remove(file) + del item['largeImage']['originalId'] + + del item['largeImage'] + + item = self.save(item) + deleted = True + self.removeThumbnailFiles(item) + return deleted
+ + +
+[docs] + def getThumbnail(self, item, checkAndCreate=False, width=None, height=None, **kwargs): + """ + Using a tile source, get a basic thumbnail. Aspect ratio is + preserved. If neither width nor height is given, a default value is + used. If both are given, the thumbnail will be no larger than either + size. + + :param item: the item with the tile source. + :param checkAndCreate: if the thumbnail is already cached, just return + True. If it does not, create, cache, and return it. If 'nosave', + return values from the cache, but do not store new results in the + cache. + :param width: maximum width in pixels. + :param height: maximum height in pixels. + :param kwargs: optional arguments. Some options are encoding, + jpegQuality, jpegSubsampling, tiffCompression, fill. This is also + passed to the tile source. + :returns: thumbData, thumbMime: the image data and the mime type OR + a generator which will yield a file. + """ + # check if a thumbnail file exists with a particular key + keydict = dict(kwargs, width=width, height=height) + return self._getAndCacheImageOrData( + item, 'getThumbnail', checkAndCreate, keydict, width=width, height=height, **kwargs)
+ + + def _getAndCacheImageOrData( + self, item, imageFunc, checkAndCreate, keydict, pickleCache=False, **kwargs): + """ + Get a file associated with an image that can be generated by a + function. + + :param item: the idem to process. + :param imageFunc: the function to call to generate a file. + :param checkAndCreate: False to return the data, creating and caching + it if needed. True to return True if the data is already in cache, + or to create the data, cache, and return it if not. 'nosave' to + return data from the cache if present, or generate the data but do + not return it if not in the cache. 'check' to just return True or + False to report if it is in the cache. + :param keydict: a dictionary of values to use for the cache key. + :param pickleCache: if True, the results of the function are pickled to + preserve them. If False, the results can be saved as a file + directly. + :params **kwargs: passed to the tile source and to the imageFunc. May + contain contentDisposition to determine how results are returned. + :returns: + """ + if 'fill' in keydict and (keydict['fill']).lower() == 'none': + del keydict['fill'] + keydict = {k: v for k, v in keydict.items() if v is not None and not k.startswith('_')} + key = json.dumps(keydict, sort_keys=True, separators=(',', ':')) + lockkey = (imageFunc, item['_id'], key) + if not hasattr(self, '_getAndCacheImageOrDataLock'): + self._getAndCacheImageOrDataLock = { + 'keys': {}, + 'lock': threading.Lock(), + } + keylock = None + with self._getAndCacheImageOrDataLock['lock']: + if lockkey in self._getAndCacheImageOrDataLock['keys']: + keylock = self._getAndCacheImageOrDataLock['keys'][lockkey] + if checkAndCreate != 'nosave' and keylock and keylock.locked(): + # This is intended to guard against calling expensive but cached + # functions multiple times. There is still a possibility of that, + # as if two calls are made close enough to concurrently, they could + # both pass this guard and then run sequentially (which is still + # preferable to concurrently). Guarding against such a race + # condition creates a bottleneck as the database checks would then + # be in the guarded code section; this is considered a reasonable + # compromise. + logger.info('Waiting for %r', (lockkey, )) + with keylock: + pass + existing = File().findOne({ + 'attachedToType': 'item', + 'attachedToId': item['_id'], + 'isLargeImageThumbnail' if not pickleCache else 'isLargeImageData': True, + 'thumbnailKey': key, + }) + if existing: + if checkAndCreate and checkAndCreate != 'nosave': + return True + if kwargs.get('contentDisposition') != 'attachment': + contentDisposition = 'inline' + else: + contentDisposition = kwargs['contentDisposition'] + if pickleCache: + data = File().open(existing).read() + return pickle.loads(data), 'application/octet-stream' + return File().download(existing, contentDisposition=contentDisposition, + headers=kwargs.get('headers', True)) + if checkAndCreate == 'check': + return False + return self.getAndCacheImageOrDataRun( + checkAndCreate, imageFunc, item, key, keydict, pickleCache, lockkey, **kwargs) + +
+[docs] + def getAndCacheImageOrDataRun( + self, checkAndCreate, imageFunc, item, key, keydict, pickleCache, lockkey, **kwargs): + """ + Actually execute a cached function. + """ + with self._getAndCacheImageOrDataLock['lock']: + if lockkey not in self._getAndCacheImageOrDataLock['keys']: + self._getAndCacheImageOrDataLock['keys'][lockkey] = threading.Lock() + keylock = self._getAndCacheImageOrDataLock['keys'][lockkey] + with keylock: + logger.debug('Computing %r', (lockkey, )) + try: + tileSource = self._loadTileSource(item, **kwargs) + result = getattr(tileSource, imageFunc)(**kwargs) + if result is None: + imageData, imageMime = b'', 'application/octet-stream' + elif pickleCache: + imageData, imageMime = result, 'application/octet-stream' + else: + imageData, imageMime = result + saveFile = True + if not pickleCache: + # The logic on which files to save could be more sophisticated. + maxThumbnailFiles = int(Setting().get( + constants.PluginSettings.LARGE_IMAGE_MAX_THUMBNAIL_FILES)) + saveFile = maxThumbnailFiles > 0 + # Make sure we don't exceed the desired number of thumbnails + self.removeThumbnailFiles( + item, maxThumbnailFiles - 1, imageKey=keydict.get('imageKey') or 'none') + if (saveFile and checkAndCreate != 'nosave' and ( + pickleCache or isinstance(imageData, bytes))): + dataStored = imageData if not pickleCache else pickle.dumps( + imageData, protocol=4) + # Save the data as a file + try: + datafile = Upload().uploadFromFile( + io.BytesIO(dataStored), size=len(dataStored), + name='_largeImageThumbnail', parentType='item', parent=item, + user=None, mimeType=imageMime, attachParent=True) + if not len(dataStored) and 'received' in datafile: + datafile = Upload().finalizeUpload( + datafile, Assetstore().load(datafile['assetstoreId'])) + datafile.update({ + 'isLargeImageThumbnail' if not pickleCache else + 'isLargeImageData': True, + 'thumbnailKey': key, + }) + # Ideally, we would check that the file is still wanted before + # we save it. This is probably impossible without true + # transactions in Mongo. + File().save(datafile) + except (GirderException, PermissionError): + logger.warning('Could not cache data for large image') + return imageData, imageMime + finally: + with self._getAndCacheImageOrDataLock['lock']: + self._getAndCacheImageOrDataLock['keys'].pop(lockkey, None)
+ + +
+[docs] + def removeThumbnailFiles(self, item, keep=0, sort=None, imageKey=None, + onlyList=False, **kwargs): + """ + Remove all large image thumbnails from an item. + + :param item: the item that owns the thumbnails. + :param keep: keep this many entries. + :param sort: the sort method used. The first (keep) records in this + sort order are kept. + :param imageKey: None for the basic thumbnail, otherwise an associated + imageKey. + :param onlyList: if True, return a list of known thumbnails or data + files that would be removed, but don't remove them. + :param kwargs: additional parameters to determine which files to + remove. + :returns: a tuple of (the number of files before removal, the number of + files removed). + """ + keys = ['isLargeImageThumbnail'] + if not keep: + keys.append('isLargeImageData') + if not sort: + sort = [('_id', SortDir.DESCENDING)] + results = [] + present = 0 + removed = 0 + for key in keys: + query = { + 'attachedToType': 'item', + 'attachedToId': item['_id'], + key: True, + } + if imageKey and key == 'isLargeImageThumbnail': + if imageKey == 'none': + query['thumbnailKey'] = {'$not': {'$regex': '"imageKey":'}} + else: + query['thumbnailKey'] = {'$regex': '"imageKey":"%s"' % imageKey} + query.update(kwargs) + for file in File().find(query, sort=sort): + present += 1 + if keep > 0: + keep -= 1 + continue + if onlyList: + results.append(file) + else: + File().remove(file) + removed += 1 + if onlyList: + return results + return (present, removed)
+ + +
+[docs] + def getRegion(self, item, **kwargs): + """ + Using a tile source, get an arbitrary region of the image, optionally + scaling the results. Aspect ratio is preserved. + + :param item: the item with the tile source. + :param kwargs: optional arguments. Some options are left, top, + right, bottom, regionWidth, regionHeight, units, width, height, + encoding, jpegQuality, jpegSubsampling, and tiffCompression. This + is also passed to the tile source. + :returns: regionData, regionMime: the image data and the mime type. + """ + tileSource = self._loadTileSource(item, **kwargs) + regionData, regionMime = tileSource.getRegion(**kwargs) + return regionData, regionMime
+ + +
+[docs] + def tileFrames(self, item, checkAndCreate='nosave', **kwargs): + """ + Given the parameters for getRegion, plus a list of frames and the + number of frames across, make a larger image composed of a region from + each listed frame composited together. + + :param item: the item with the tile source. + :param checkAndCreate: if False, use the cache. If True and the result + is already cached, just return True. If is not, create, cache, and + return it. If 'nosave', return values from the cache, but do not + store new results in the cache. + :param kwargs: optional arguments. Some options are left, top, + right, bottom, regionWidth, regionHeight, units, width, height, + encoding, jpegQuality, jpegSubsampling, and tiffCompression. This + is also passed to the tile source. These also include frameList + and framesAcross. + :returns: regionData, regionMime: the image data and the mime type. + """ + imageKey = 'tileFrames' + return self._getAndCacheImageOrData( + item, 'tileFrames', checkAndCreate, + dict(kwargs, imageKey=imageKey), headers=False, **kwargs)
+ + +
+[docs] + def getPixel(self, item, **kwargs): + """ + Using a tile source, get a single pixel from the image. + + :param item: the item with the tile source. + :param kwargs: optional arguments. Some options are left, top. + :returns: a dictionary of the color channel values, possibly with + additional information + """ + tileSource = self._loadTileSource(item, **kwargs) + return tileSource.getPixel(**kwargs)
+ + +
+[docs] + def histogram(self, item, checkAndCreate=False, **kwargs): + """ + Using a tile source, get a histogram of the image. + + :param item: the item with the tile source. + :param kwargs: optional arguments. See the tilesource histogram + method. + :returns: histogram object. + """ + if kwargs.get('range') is not None and kwargs.get('range') != 'round': + tileSource = self._loadTileSource(item, **kwargs) + result = tileSource.histogram(**kwargs) + else: + imageKey = 'histogram' + result = self._getAndCacheImageOrData( + item, 'histogram', checkAndCreate, + dict(kwargs, imageKey=imageKey), pickleCache=True, **kwargs) + if not isinstance(result, bool): + result = result[0] + return result
+ + +
+[docs] + def getBandInformation(self, item, statistics=True, **kwargs): + """ + Using a tile source, get band information of the image. + + :param item: the item with the tile source. + :param kwargs: optional arguments. See the tilesource + getBandInformation method. + :returns: band information. + """ + tileSource = self._loadTileSource(item, **kwargs) + result = tileSource.getBandInformation(statistics=statistics, **kwargs) + return result
+ + +
+[docs] + def tileSource(self, item, **kwargs): + """ + Get a tile source for an item. + + :param item: the item with the tile source. + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + return self._loadTileSource(item, **kwargs)
+ + +
+[docs] + def getAssociatedImagesList(self, item, **kwargs): + """ + Return a list of associated images. + + :param item: the item with the tile source. + :return: a list of keys of associated images. + """ + tileSource = self._loadTileSource(item, **kwargs) + return tileSource.getAssociatedImagesList()
+ + +
+[docs] + def getAssociatedImage(self, item, imageKey, checkAndCreate=False, *args, **kwargs): + """ + Return an associated image. + + :param item: the item with the tile source. + :param imageKey: the key of the associated image to retrieve. + :param kwargs: optional arguments. Some options are width, height, + encoding, jpegQuality, jpegSubsampling, and tiffCompression. + :returns: imageData, imageMime: the image data and the mime type, or + None if the associated image doesn't exist. + """ + keydict = dict(kwargs, imageKey=imageKey) + return self._getAndCacheImageOrData( + item, 'getAssociatedImage', checkAndCreate, keydict, imageKey=imageKey, **kwargs)
+ + + def _scheduleTileFrames(self, item, tileFramesList, user): + """ + Schedule generating tile frames in a local job. + + :param item: the item. + :param tileFramesList: a list of dictionary of parameters to pass to + the tileFrames method. + :param user: the user owning the job. + """ + job = Job().createLocalJob( + module='large_image_tasks.tasks', + function='cache_tile_frames_job', + kwargs={ + 'itemId': str(item['_id']), + 'tileFramesList': tileFramesList, + }, + title='Cache tileFrames', + type='large_image_cache_tile_frames', + user=user, + public=True, + asynchronous=True, + ) + Job().scheduleJob(job) + return job + + def _scheduleHistograms(self, item, histogramList, user): + """ + Schedule generating histograms in a local job. + + :param item: the item. + :param histogramList: a list of dictionary of parameters to pass to + the histogram method. + :param user: the user owning the job. + """ + job = Job().createLocalJob( + module='large_image_tasks.tasks', + function='cache_histograms_job', + kwargs={ + 'itemId': str(item['_id']), + 'histogramList': histogramList, + }, + title='Cache Histograms', + type='large_image_cache_histograms', + user=user, + public=True, + asynchronous=True, + ) + Job().scheduleJob(job) + return job
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image/rest.html b/_modules/girder_large_image/rest.html new file mode 100644 index 000000000..cebc16f89 --- /dev/null +++ b/_modules/girder_large_image/rest.html @@ -0,0 +1,281 @@ + + + + + + girder_large_image.rest — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image.rest

+import json
+
+from girder import logger
+from girder.api import access
+from girder.api.describe import Description, autoDescribeRoute
+from girder.api.rest import boundHandler
+from girder.constants import AccessType, TokenScope
+from girder.models.folder import Folder
+from girder.models.item import Item
+
+
+
+[docs] +def addSystemEndpoints(apiRoot): + """ + This adds endpoints to routes that already exist in Girder. + + :param apiRoot: Girder api root class. + """ + apiRoot.folder.route('GET', (':id', 'yaml_config', ':name'), getYAMLConfigFile) + apiRoot.folder.route('PUT', (':id', 'yaml_config', ':name'), putYAMLConfigFile) + + origItemFind = apiRoot.item._find + origFolderFind = apiRoot.folder._find + + @boundHandler(apiRoot.item) + def altItemFind(self, folderId, text, name, limit, offset, sort, filters=None): + if sort and sort[0][0][0] == '[': + sort = json.loads(sort[0][0]) + recurse = False + if text and text.startswith('_recurse_:'): + recurse = True + text = text.split('_recurse_:', 1)[1] + if filters is None and text and text.startswith('_filter_:'): + try: + filters = json.loads(text.split('_filter_:', 1)[1].strip()) + text = None + except Exception as exc: + logger.warning('Failed to parse _filter_ from text field: %r', exc) + if filters: + try: + logger.debug('Item find filters: %s', json.dumps(filters)) + except Exception: + pass + if recurse: + return _itemFindRecursive( + self, origItemFind, folderId, text, name, limit, offset, sort, filters) + return origItemFind(folderId, text, name, limit, offset, sort, filters) + + @boundHandler(apiRoot.item) + def altFolderFind(self, parentType, parentId, text, name, limit, offset, sort, filters=None): + if sort and sort[0][0][0] == '[': + sort = json.loads(sort[0][0]) + return origFolderFind(parentType, parentId, text, name, limit, offset, sort, filters) + + if not hasattr(origItemFind, '_origFunc'): + apiRoot.item._find = altItemFind + altItemFind._origFunc = origItemFind + apiRoot.folder._find = altFolderFind + altFolderFind._origFunc = origFolderFind
+ + + +def _itemFindRecursive(self, origItemFind, folderId, text, name, limit, offset, sort, filters): + """ + If a recursive search within a folderId is specified, use an aggregation to + find all folders that are descendants of the specified folder. If there + are any, then perform a search that matches any of those folders rather + than just the parent. + + :param self: A reference to the Item() resource record. + :param origItemFind: the original _find method, used as a fallback. + + For the remaining parameters, see girder/api/v1/item._find + """ + from bson.objectid import ObjectId + + if folderId: + pipeline = [ + {'$match': {'_id': ObjectId(folderId)}}, + {'$graphLookup': { + 'from': 'folder', + 'connectFromField': '_id', + 'connectToField': 'parentId', + 'depthField': '_depth', + 'as': '_folder', + 'startWith': '$_id', + }}, + {'$group': {'_id': '$_folder._id'}}, + ] + children = [ObjectId(folderId)] + next(Folder().collection.aggregate(pipeline))['_id'] + if len(children) > 1: + filters = (filters.copy() if filters else {}) + if text: + filters['$text'] = { + '$search': text, + } + if name: + filters['name'] = name + filters['folderId'] = {'$in': children} + user = self.getCurrentUser() + if isinstance(sort, list): + sort.append(('parentId', 1)) + return Item().findWithPermissions(filters, offset, limit, sort=sort, user=user) + return origItemFind(folderId, text, name, limit, offset, sort, filters) + + +
+[docs] +@access.public(scope=TokenScope.DATA_READ) +@autoDescribeRoute( + Description('Get a config file.') + .notes( + 'This walks up the chain of parent folders until the file is found. ' + 'If not found, the .config folder in the parent collection or user is ' + 'checked.\n\nAny yaml file can be returned. If the top-level is a ' + 'dictionary and contains keys "access" or "groups" where those are ' + 'dictionaries, the returned value will be modified based on the ' + 'current user. The "groups" dictionary contains keys that are group ' + 'names and values that update the main dictionary. All groups that ' + 'the user is a member of are merged in alphabetical order. If a key ' + 'and value of "\\__all\\__": True exists, the replacement is total; ' + 'otherwise it is a merge. If the "access" dictionary exists, the ' + '"user" and "admin" subdictionaries are merged if a calling user is ' + 'present and if the user is an admin, respectively (both get merged ' + 'for admins).') + .modelParam('id', model=Folder, level=AccessType.READ) + .param('name', 'The name of the file.', paramType='path') + .errorResponse(), +) +@boundHandler() +def getYAMLConfigFile(self, folder, name): + from .. import yamlConfigFile + + user = self.getCurrentUser() + return yamlConfigFile(folder, name, user)
+ + + +
+[docs] +@access.public(scope=TokenScope.DATA_WRITE) +@autoDescribeRoute( + Description('Get a config file.') + .notes( + 'This replaces or creates an item in the specified folder with the ' + 'specified name containing a single file also of the specified ' + 'name. The file is added to the default assetstore, and any existing ' + 'file may be permanently deleted.') + .modelParam('id', model=Folder, level=AccessType.WRITE) + .param('name', 'The name of the file.', paramType='path') + .param('config', 'The contents of yaml config file to validate.', + paramType='body'), +) +@boundHandler() +def putYAMLConfigFile(self, folder, name, config): + from .. import yamlConfigFileWrite + + user = self.getCurrentUser() + config = config.read().decode('utf8') + return yamlConfigFileWrite(folder, name, user, config)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image/rest/item_meta.html b/_modules/girder_large_image/rest/item_meta.html new file mode 100644 index 000000000..1d3716679 --- /dev/null +++ b/_modules/girder_large_image/rest/item_meta.html @@ -0,0 +1,232 @@ + + + + + + girder_large_image.rest.item_meta — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image.rest.item_meta

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+from girder.api import access
+from girder.api.describe import Description, describeRoute
+from girder.api.rest import loadmodel
+from girder.api.v1.item import Item
+from girder.constants import AccessType, TokenScope
+
+
+
+[docs] +class InternalMetadataItemResource(Item): + def __init__(self, apiRoot): + super().__init__() + apiRoot.item.route( + 'GET', (':itemId', 'internal_metadata', ':key'), self.getMetadataKey, + ) + apiRoot.item.route( + 'PUT', (':itemId', 'internal_metadata', ':key'), self.updateMetadataKey, + ) + apiRoot.item.route( + 'DELETE', (':itemId', 'internal_metadata', ':key'), self.deleteMetadataKey, + ) + +
+[docs] + @describeRoute( + Description('Get the value for a single internal metadata key on this item.') + .param('itemId', 'The ID of the item.', paramType='path') + .param( + 'key', + 'The metadata key to retrieve.', + paramType='path', + default='meta', + ) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public() + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getMetadataKey(self, item, key, params): + if key not in item: + return None + return item[key]
+ + +
+[docs] + @describeRoute( + Description( + 'Overwrite the value for a single internal metadata key on this item.', + ) + .param('itemId', 'The ID of the item.', paramType='path') + .param( + 'key', + 'The metadata key which should have a new value. \ + The default key, "meta" is equivalent to the external metadata. \ + Editing the "meta" key is equivalent to using PUT /item/{id}/metadata.', + paramType='path', + default='meta', + ) + .param( + 'value', + 'The new value that should be written for the chosen metadata key', + paramType='body', + ) + .errorResponse('ID was invalid.') + .errorResponse('Write access was denied for the item.', 403), + ) + @access.user(scope=TokenScope.DATA_WRITE) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.WRITE) + def updateMetadataKey(self, item, key, params): + item[key] = self.getBodyJson() + self._model.save(item)
+ + +
+[docs] + @describeRoute( + Description('Delete a single internal metadata key on this item.') + .param('itemId', 'The ID of the item.', paramType='path') + .param( + 'key', + 'The metadata key to delete.', + paramType='path', + default='meta', + ) + .errorResponse('ID was invalid.') + .errorResponse('Write access was denied for the item.', 403), + ) + @access.user(scope=TokenScope.DATA_WRITE) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def deleteMetadataKey(self, item, key, params): + if key in item: + del item[key] + self._model.save(item)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image/rest/large_image_resource.html b/_modules/girder_large_image/rest/large_image_resource.html new file mode 100644 index 000000000..d1e5ff2b3 --- /dev/null +++ b/_modules/girder_large_image/rest/large_image_resource.html @@ -0,0 +1,971 @@ + + + + + + girder_large_image.rest.large_image_resource — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image.rest.large_image_resource

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import concurrent.futures
+import datetime
+import io
+import json
+import os
+import pprint
+import re
+import shutil
+import threading
+import time
+
+import cherrypy
+import pymongo
+from girder_jobs.constants import JobStatus
+from girder_jobs.models.job import Job
+
+import large_image
+from girder import logger
+from girder.api import access
+from girder.api.describe import Description, autoDescribeRoute, describeRoute
+from girder.api.rest import Resource
+from girder.constants import AccessType, SortDir, TokenScope
+from girder.exceptions import RestException
+from girder.models.file import File
+from girder.models.folder import Folder
+from girder.models.item import Item
+from girder.models.setting import Setting
+from girder.utility import toBool
+from large_image import cache_util
+from large_image.exceptions import TileGeneralError, TileSourceError
+
+from .. import constants, girder_tilesource
+from ..models.image_item import ImageItem
+
+
+
+[docs] +def createThumbnailsJobTask(item, spec): + """ + For an individual item, check or create all of the appropriate thumbnails. + + :param item: the image item. + :param spec: a list of thumbnail specifications. + :returns: a dictionary with the total status of the thumbnail job. + """ + status = {'checked': 0, 'created': 0, 'failed': 0} + for entry in spec: + try: + if entry.get('imageKey'): + result = ImageItem().getAssociatedImage(item, checkAndCreate=True, **entry) + else: + result = ImageItem().getThumbnail(item, checkAndCreate=True, **entry) + status['checked' if result is True else 'created'] += 1 + except TileGeneralError as exc: + status['failed'] += 1 + status['lastFailed'] = str(item['_id']) + logger.info('Failed to get thumbnail for item %s: %r' % (item['_id'], exc)) + except AttributeError: + raise + except Exception: + status['failed'] += 1 + status['lastFailed'] = str(item['_id']) + logger.exception( + 'Unexpected exception when trying to create a thumbnail for item %s' % + item['_id']) + return status
+ + + +
+[docs] +def createThumbnailsJobLog(job, info, prefix='', status=None): + """ + Log information about the create thumbnails job. + + :param job: the job object. + :param info: a dictionary with the number of thumbnails checked, created, + and failed. + :param prefix: a string to place in front of the log message. + :param status: if not None, a new status for the job. + """ + msg = prefix + 'Checked %d, created %d thumbnail files' % ( + info['checked'], info['created']) + if prefix == '' and info.get('items', 0) * info.get('specs', 0): + done = info['checked'] + info['created'] + info['failed'] + if done < info['items'] * info['specs']: + msg += ' (estimated %4.2f%% done)' % ( + 100.0 * done / (info['items'] * info['specs'])) + msg += '\n' + if info['failed']: + msg += 'Failed on %d thumbnail file%s (last failure on item %s)\n' % ( + info['failed'], + 's' if info['failed'] != 1 else '', info['lastFailed']) + job = Job().updateJob(job, log=msg, status=status) + return job, msg
+ + + +
+[docs] +def cursorNextOrNone(cursor): + """ + Given a Mongo cursor, return the next value if there is one. If not, + return None. + + :param cursor: a cursor to get a value from. + :returns: the next value or None. + """ + try: + return cursor.next() + except StopIteration: + return None
+ + + +
+[docs] +def createThumbnailsJob(job): + thread = threading.Thread(target=createThumbnailsJobThread, args=(job, ), daemon=True) + thread.start()
+ + + +
+[docs] +def createThumbnailsJobThread(job): # noqa + """ + Create thumbnails for all of the large image items. + + The job object contains:: + + - spec: an array, each entry of which is the parameter dictionary + for the model getThumbnail function. + - logInterval: the time in seconds between log messages. This + also controls the granularity of cancelling the job. + - concurrent: the number of threads to use. 0 for the number of + cpus. + + :param job: the job object including kwargs. + """ + job = Job().updateJob( + job, log='Started creating large image thumbnails\n', + status=JobStatus.RUNNING) + concurrency = int(job['kwargs'].get('concurrent', 0)) + concurrency = large_image.config.cpu_count( + logical=True) if concurrency < 1 else concurrency + status = { + 'checked': 0, + 'created': 0, + 'failed': 0, + } + + spec = job['kwargs']['spec'] + logInterval = float(job['kwargs'].get('logInterval', 10)) + job = Job().updateJob(job, log='Creating thumbnails (%d concurrent)\n' % concurrency) + nextLogTime = time.time() + logInterval + tasks = [] + # This could be switched from ThreadPoolExecutor to ProcessPoolExecutor + # without any other changes. Doing so would probably improve parallel + # performance, but may not work reliably under Python 2.x. + pool = concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) + try: + # Get a cursor with the list of images + query = {'largeImage.fileId': {'$exists': True}} + sort = [('_id', SortDir.ASCENDING)] + items = Item().find(query, sort=sort) + if hasattr(items, 'count'): + status['items'] = items.count() + status['specs'] = len(spec) + nextitem = cursorNextOrNone(items) + while len(tasks) or nextitem is not None: + # Create more tasks than we strictly need so if one finishes before + # we check another will be ready. This is balanced with not + # creating too many to avoid excessive memory use. As such, we + # can't do a simple iteration over the database cursor, as it will + # be exhausted before we are done. + while len(tasks) < concurrency * 4 and nextitem is not None: + tasks.append(pool.submit(createThumbnailsJobTask, nextitem, spec)) + try: + nextitem = cursorNextOrNone(items) + except pymongo.errors.CursorNotFound: + # If the process takes long enough, the cursor is removed. + # In this case, redo the query and keep going. + items = Item().find(query, sort=sort) + nextitem = cursorNextOrNone(items) + if nextitem is not None: + query['_id'] = {'$gt': nextitem['_id']} + # Wait a short time or until the oldest task is complete + try: + tasks[0].result(0.1) + except concurrent.futures.TimeoutError: + pass + # Remove completed tasks from our list, adding their results to the + # status. + for pos in range(len(tasks) - 1, -1, -1): + if tasks[pos].done(): + r = tasks[pos].result() + status['created'] += r['created'] + status['checked'] += r['checked'] + status['failed'] += r['failed'] + status['lastFailed'] = r.get('lastFailed', status.get('lastFailed')) + tasks[pos:pos + 1] = [] + # Periodically, log the state of the job and check if it was + # deleted or canceled. + if time.time() > nextLogTime: + job, msg = createThumbnailsJobLog(job, status) + # Check if the job was deleted or canceled; if so, quit + job = Job().load(id=job['_id'], force=True) + if not job or job['status'] in (JobStatus.CANCELED, JobStatus.ERROR): + cause = { + None: 'deleted', + JobStatus.CANCELED: 'canceled', + JobStatus.ERROR: 'stopped due to error', + }[None if not job else job.get('status')] + msg = 'Large image thumbnails job %s' % cause + logger.info(msg) + # Cancel any outstanding tasks. If they haven't started, + # they are discarded. Those that have started will still + # run, though. + for task in tasks: + task.cancel() + return + nextLogTime = time.time() + logInterval + except Exception: + logger.exception('Error with large image create thumbnails job') + Job().updateJob( + job, log='Error creating large image thumbnails\n', + status=JobStatus.ERROR) + return + finally: + # Clean up the task pool asynchronously + pool.shutdown(False) + job, msg = createThumbnailsJobLog(job, status, 'Finished: ', JobStatus.SUCCESS) + logger.info(msg)
+ + + +
+[docs] +class LargeImageResource(Resource): + + def __init__(self): + super().__init__() + + self.resourceName = 'large_image' + self.route('GET', ('cache', ), self.cacheInfo) + self.route('PUT', ('cache', 'clear'), self.cacheClear) + self.route('POST', ('config', 'format'), self.configFormat) + self.route('POST', ('config', 'validate'), self.configValidate) + self.route('POST', ('config', 'replace'), self.configReplace) + self.route('GET', ('settings',), self.getPublicSettings) + self.route('GET', ('sources',), self.listSources) + self.route('GET', ('thumbnails',), self.countThumbnails) + self.route('PUT', ('thumbnails',), self.createThumbnails) + self.route('DELETE', ('thumbnails',), self.deleteThumbnails) + self.route('GET', ('associated_images',), self.countAssociatedImages) + self.route('DELETE', ('associated_images',), self.deleteAssociatedImages) + self.route('GET', ('histograms',), self.countHistograms) + self.route('DELETE', ('histograms',), self.deleteHistograms) + self.route('DELETE', ('tiles', 'incomplete'), self.deleteIncompleteTiles) + self.route('PUT', ('folder', ':id', 'tiles'), self.createLargeImages) + +
+[docs] + @describeRoute( + Description('Clear tile source caches to release resources and file handles.'), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def cacheClear(self, params): + import gc + + before = cache_util.cachesInfo() + cache_util.cachesClear() + after = cache_util.cachesInfo() + # Add a small delay to give the memcached time to clear + stoptime = time.time() + 5 + while time.time() < stoptime and any(after[key]['used'] for key in after): + time.sleep(0.1) + after = cache_util.cachesInfo() + gc.collect() + return { + 'cacheCleared': datetime.datetime.now(datetime.timezone.utc), + 'before': before, + 'after': after, + }
+ + +
+[docs] + @describeRoute( + Description('Get information on caches.'), + ) + @access.admin(scope=TokenScope.DATA_READ) + def cacheInfo(self, params): + return cache_util.cachesInfo()
+ + +
+[docs] + @describeRoute( + Description('Get public settings for large image display.'), + ) + @access.public(scope=TokenScope.DATA_READ) + def getPublicSettings(self, params): + keys = [getattr(constants.PluginSettings, key) + for key in dir(constants.PluginSettings) + if key.startswith('LARGE_IMAGE_')] + return {k: Setting().get(k) for k in keys}
+ + +
+[docs] + @describeRoute( + Description('Count the number of cached thumbnail files for ' + 'large_image items.') + .param('spec', 'A JSON list of thumbnail specifications to count. ' + 'If empty, all cached thumbnails are counted. The ' + 'specifications typically include width, height, encoding, and ' + 'encoding options.', required=False), + ) + @access.admin(scope=TokenScope.DATA_READ) + def countThumbnails(self, params): + return self._countCachedImages(params.get('spec'))
+ + +
+[docs] + @describeRoute( + Description('Count the number of cached associated image files for ' + 'large_image items.') + .param('imageKey', 'If specific, only include images with the ' + 'specified key', required=False) + .notes('The imageKey can also be "tileFrames".'), + ) + @access.admin(scope=TokenScope.DATA_READ) + def countAssociatedImages(self, params): + return self._countCachedImages( + None, associatedImages=True, imageKey=params.get('imageKey'))
+ + + def _countCachedImages(self, spec, associatedImages=False, imageKey=None): + if spec is not None: + try: + spec = json.loads(spec) + if not isinstance(spec, list): + raise ValueError + except ValueError: + msg = 'The spec parameter must be a JSON list.' + raise RestException(msg) + spec = [json.dumps(entry, sort_keys=True, separators=(',', ':')) + for entry in spec] + else: + spec = [None] + count = 0 + for entry in spec: + query = {'isLargeImageThumbnail': True, 'attachedToType': 'item'} + if entry is not None: + query['thumbnailKey'] = entry + elif associatedImages: + if imageKey and re.match(r'^[0-9A-Za-z].*$', imageKey): + query['thumbnailKey'] = {'$regex': '"imageKey":"%s"' % imageKey} + else: + query['thumbnailKey'] = {'$regex': '"imageKey":'} + count += File().find(query).count() + return count + +
+[docs] + @describeRoute( + Description('Create cached thumbnail files from large_image items.') + .notes('This creates a local job that processes all large_image ' + 'items. A common spec for the Girder API is: [{"width": 160, ' + '"height": 100}, {"width": 160, "height": 100, "imageKey": ' + '"macro"}, {"width": 160, "height": 100, "imageKey": "label"}]') + .param('spec', 'A JSON list of thumbnail specifications to create. ' + 'The specifications typically include width, height, encoding, ' + 'and encoding options.') + .param('logInterval', 'The number of seconds between log messages. ' + 'This also determines how often the creation job is checked if ' + 'it has been canceled or deleted. A value of 0 will log after ' + 'each thumbnail is checked or created.', required=False, + dataType='float') + .param('concurrent', 'The number of concurrent threads to use when ' + 'making thumbnails. 0 or unspecified to base this on the ' + 'number of reported cpus.', required=False, dataType='int'), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def createThumbnails(self, params): + self.requireParams(['spec'], params) + try: + spec = json.loads(params['spec']) + if not isinstance(spec, list): + raise ValueError + except ValueError: + msg = 'The spec parameter must be a JSON list.' + raise RestException(msg) + maxThumbnailFiles = int(Setting().get( + constants.PluginSettings.LARGE_IMAGE_MAX_THUMBNAIL_FILES)) + if maxThumbnailFiles <= 0: + msg = 'Thumbnail files are not enabled.' + raise RestException(msg) + jobKwargs = {'spec': spec} + if params.get('logInterval') is not None: + jobKwargs['logInterval'] = float(params['logInterval']) + if params.get('concurrent') is not None: + jobKwargs['concurrent'] = float(params['concurrent']) + job = Job().createLocalJob( + module='girder_large_image.rest.large_image_resource', + function='createThumbnailsJob', + kwargs=jobKwargs, + title='Create large image thumbnail files.', + type='large_image_create_thumbnails', + user=self.getCurrentUser(), + public=True, + asynchronous=True, + ) + Job().scheduleJob(job) + return job
+ + +
+[docs] + @describeRoute( + Description('Delete cached thumbnail files from large_image items.') + .param('spec', 'A JSON list of thumbnail specifications to delete. ' + 'If empty, all cached thumbnails are deleted. The ' + 'specifications typically include width, height, encoding, and ' + 'encoding options.', required=False), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def deleteThumbnails(self, params): + return self._deleteCachedImages(params.get('spec'))
+ + +
+[docs] + @describeRoute( + Description('Delete cached associated image files from large_image items.') + .param('imageKey', 'If specific, only include images with the ' + 'specified key', required=False), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def deleteAssociatedImages(self, params): + return self._deleteCachedImages( + None, associatedImages=True, imageKey=params.get('imageKey'))
+ + + def _deleteCachedImages(self, spec, associatedImages=False, imageKey=None): + if spec is not None: + try: + spec = json.loads(spec) + if not isinstance(spec, list): + raise ValueError + except ValueError: + msg = 'The spec parameter must be a JSON list.' + raise RestException(msg) + spec = [json.dumps(entry, sort_keys=True, separators=(',', ':')) + for entry in spec] + else: + spec = [None] + removed = 0 + for entry in spec: + query = {'isLargeImageThumbnail': True, 'attachedToType': 'item'} + if entry is not None: + query['thumbnailKey'] = entry + elif associatedImages: + if imageKey and re.match(r'^[0-9A-Za-z].*$', imageKey): + query['thumbnailKey'] = {'$regex': '"imageKey":"%s"' % imageKey} + else: + query['thumbnailKey'] = {'$regex': '"imageKey":'} + for file in File().find(query): + File().remove(file) + removed += 1 + return removed + +
+[docs] + @access.user(scope=TokenScope.DATA_WRITE) + @autoDescribeRoute( + Description('Create large images for all items within a folder.') + .notes('Does not work for new items with multiple files.') + .modelParam('id', 'The ID of the folder.', model=Folder, + level=AccessType.WRITE, required=True) + .param('createJobs', 'If true, a job will be used to create the image ' + 'when needed; if always, a job will always be used; if false, ' + 'a job will never be used, creating a version of the image in ' + 'a preferred format.', dataType='string', default='false', + required=False, enum=['true', 'false', 'always']) + .param('localJobs', 'If true, run each creation job locally; if false, ' + 'run via the remote worker.', dataType='boolean', default='false', + required=False) + .param('recurse', 'If true, items in child folders will also be checked.', + dataType='boolean', default=False, required=False) + .param('cancelJobs', 'If true, unfinished large image job(s) associated ' + 'with items in the folder will be canceled, then a new large ' + 'image created; if false, items with an unfinished large image ' + 'will be skipped.', dataType='boolean', default=False, required=False) + .param('redoExisting', 'If true, existing large images should be removed and ' + 'recreated. Otherwise they will be skipped.', dataType='boolean', + default=False, required=False) + .errorResponse('ID was invalid.') + .errorResponse('Write access was denied for the folder.', 403), + ) + def createLargeImages(self, folder, createJobs, localJobs, recurse, cancelJobs, redoExisting): + user = self.getCurrentUser() + if createJobs != 'always': + createJobs = toBool(createJobs) + return self._createLargeImagesRecurse( + folder=folder, user=user, recurse=recurse, createJobs=createJobs, + localJobs=localJobs, cancelJobs=cancelJobs, redo=redoExisting)
+ + + def _createLargeImagesRecurse( + self, folder, user, recurse, createJobs, localJobs, cancelJobs, + redo, result=None): + if result is None: + result = {'childFoldersRecursed': 0, + 'itemsSkipped': 0, + 'jobsCanceled': 0, + 'jobsFailedToCancel': 0, + 'largeImagesCreated': 0, + 'largeImagesNotCreated': 0, + 'largeImagesRemovedAndRecreated': 0, + 'largeImagesRemovedAndNotRecreated': 0, + 'totalItems': 0} + if recurse: + for childFolder in Folder().childFolders(parent=folder, parentType='folder'): + result['childFoldersRecursed'] += 1 + self.createLargeImagesRecurse( + childFolder, user, recurse, createJobs, localJobs, + cancelJobs, redo, result) + for item in Folder().childItems(folder=folder): + result['totalItems'] += 1 + self._createLargeImagesItem( + item, user, createJobs, localJobs, cancelJobs, redo, result) + return result + + def _createLargeImagesItem( + self, item, user, createJobs, localJobs, cancelJobs, redo, result): + if item.get('largeImage'): + previousFileId = item['largeImage'].get('originalId', item['largeImage']['fileId']) + if item['largeImage'].get('expected'): + if not cancelJobs: + result['itemsSkipped'] += 1 + return + job = Job().load(item['largeImage']['jobId'], force=True) + if job and job.get('status') in { + JobStatus.QUEUED, JobStatus.RUNNING, JobStatus.INACTIVE}: + job = Job().cancelJob(job) + if job and job.get('status') in { + JobStatus.QUEUED, JobStatus.RUNNING, JobStatus.INACTIVE}: + result['jobsFailedToCancel'] += 1 + result['itemsSkipped'] += 1 + return + result['jobsCanceled'] += 1 + else: + try: + ImageItem().getMetadata(item) + if not redo: + result['itemsSkipped'] += 1 + return + except (TileSourceError, KeyError): + pass + ImageItem().delete(item) + try: + ImageItem().createImageItem( + item, File().load(user=user, id=previousFileId), + createJob=createJobs, localJob=localJobs) + result['largeImagesRemovedAndRecreated'] += 1 + except Exception: + result['largeImagesRemovedAndNotRecreated'] += 1 + else: + files = list(Item().childFiles(item=item, limit=2)) + if len(files) == 1: + try: + ImageItem().createImageItem( + item, files[0], createJob=createJobs, localJob=localJobs) + result['largeImagesCreated'] += 1 + except Exception: + result['largeImagesNotCreated'] += 1 + else: + result['itemsSkipped'] += 1 + +
+[docs] + @describeRoute( + Description('Remove large images from items where the large image job ' + 'incomplete.') + .notes('This is used to clean up all large image conversion jobs that ' + 'have failed to complete. If a job is in progress, it will be ' + 'cancelled. The return value is the number of items that were ' + 'adjusted.'), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def deleteIncompleteTiles(self, params): + result = {'removed': 0} + while True: + item = Item().findOne({'largeImage.expected': True}) + if not item: + break + job = Job().load(item['largeImage']['jobId'], force=True) + if job and job.get('status') in ( + JobStatus.QUEUED, JobStatus.RUNNING): + job = Job().cancelJob(job) + if job and job.get('status') in ( + JobStatus.QUEUED, JobStatus.RUNNING): + result['message'] = ('The job for item %s could not be ' + 'canceled' % (str(item['_id']))) + break + ImageItem().delete(item) + result['removed'] += 1 + return result
+ + +
+[docs] + @describeRoute( + Description('List all Girder tile sources with associated extensions, ' + 'mime types, and versions. Lower values indicate a ' + 'higher priority for an extension or mime type with that ' + 'source.'), + ) + @access.public(scope=TokenScope.DATA_READ) + def listSources(self, params): + return large_image.tilesource.listSources( + girder_tilesource.AvailableGirderTileSources)['sources']
+ + +
+[docs] + @describeRoute( + Description('Count the number of cached histograms for large_image items.'), + ) + @access.admin(scope=TokenScope.DATA_READ) + def countHistograms(self, params): + query = { + 'isLargeImageData': True, + 'attachedToType': 'item', + 'thumbnailKey': {'$regex': '"imageKey":"histogram"'}, + } + count = File().find(query).count() + return count
+ + +
+[docs] + @describeRoute( + Description('Delete cached histograms from large_image items.'), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def deleteHistograms(self, params): + query = { + 'isLargeImageData': True, + 'attachedToType': 'item', + 'thumbnailKey': {'$regex': '"imageKey":"histogram"'}, + } + removed = 0 + for file in File().find(query): + File().remove(file) + removed += 1 + return removed
+ + + def _configValidateException(self, exc, lineno=None): + """ + Report a config validation exception with a line number. + """ + try: + msg = str(exc) + matches = re.search(r'line: (\d+)', msg) + if not matches: + matches = re.search(r'\[line[ ]*(\d+)\]', msg) + if matches: + line = int(matches.groups()[0]) + msg = msg.split('\n')[0].strip() or 'General error' + msg = msg.rsplit(": '<string>'", 1)[0].rsplit("'<string>'", 1)[-1].strip() + return [{'line': line - 1, 'message': msg}] + except Exception: + pass + if lineno is not None: + return [{'line': lineno, 'message': str(exc)}] + return [{'line': 0, 'message': 'General error'}] + + def _configValidate(self, config): + """ + Check if a Girder config file will validate. If not, return an + array of lines where it fails to validate. + + :param config: The string representation of the config file to + validate. + :returns: a list of errors, though usually only the first one. + """ + parser = cherrypy.lib.reprconf.Parser() + try: + parser.read_string(config) + except Exception as exc: + return self._configValidateException(exc) + err = None + try: + parser.as_dict() + return [] + except Exception as exc: + err = exc + try: + parser.as_dict(raw=True) + return self._configValidateException(exc) + except Exception: + pass + lines = io.StringIO(config).readlines() + for pos in range(len(lines), 0, -1): + try: + parser = cherrypy.lib.reprconf.Parser() + parser.read_string(''.join(lines[:pos])) + parser.as_dict() + return self._configValidateException('Config values must be valid Python.', pos) + except Exception: + pass + return self._configValidateException(err) + +
+[docs] + @autoDescribeRoute( + Description('Validate a Girder config file') + .notes('Returns a list of errors found.') + .param('config', 'The contents of config file to validate.', + paramType='body'), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def configValidate(self, config): + config = config.read().decode('utf8') + return self._configValidate(config)
+ + +
+[docs] + @autoDescribeRoute( + Description('Reformat a Girder config file') + .param('config', 'The contents of config file to format.', + paramType='body'), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def configFormat(self, config): + config = config.read().decode('utf8') + if len(self._configValidate(config)): + return config + # reformat here + # collect comments + comments = ['[__comment__]\n'] + for line in io.StringIO(config): + if line.strip()[:1] in {'#', ';'}: + line = '__comment__%d = %r\n' % (len(comments), line) + # If a comment is in the middle of a value, hoist it up + for pos in range(len(comments), 0, -1): + try: + parser = cherrypy.lib.reprconf.Parser() + parser.read_string(''.join(comments[:pos])) + parser.as_dict(raw=True) + comments[pos:pos] = [line] + break + except Exception: + pass + else: + comments.append(line) + parser = cherrypy.lib.reprconf.Parser() + parser.read_string(''.join(comments)) + results = parser.as_dict(raw=True) + # Build results + out = [] + for section in results: + if section != '__comment__': + out.append('[%s]\n' % section) + for key, val in results[section].items(): + if not key.startswith('__comment__'): + valstr = repr(val) + if len(valstr) + len(key) + 3 >= 79: + valstr = pprint.pformat( + val, width=79, indent=2, compact=True, sort_dicts=False) + out.append('%s = %s\n' % (key, valstr)) + else: + out.append(val) + if section != '__comment__': + out.append('\n') + return ''.join(out)
+ + +
+[docs] + @autoDescribeRoute( + Description('Replace the existing Girder config file') + .param('restart', 'Whether to restart the server after updating the ' + 'config file', required=False, dataType='boolean', default=True) + .param('config', 'The new contents of config file.', + paramType='body'), + ) + @access.admin(scope=TokenScope.USER_AUTH) + def configReplace(self, config, restart): + config = config.read().decode('utf8') + if len(self._configValidate(config)): + msg = 'Invalid config file' + raise RestException(msg) + path = os.path.join(os.path.expanduser('~'), '.girder', 'girder.cfg') + if 'GIRDER_CONFIG' in os.environ: + path = os.environ['GIRDER_CONFIG'] + if os.path.exists(path): + contents = open(path).read() + if contents == config: + return {'status': 'no change'} + newpath = path + '.' + time.strftime( + '%Y%m%d-%H%M%S', time.localtime(os.stat(path).st_mtime)) + logger.info('Copying existing config file from %s to %s' % (path, newpath)) + shutil.copy2(path, newpath) + logger.warning('Replacing config file %s' % (path)) + open(path, 'w').write(config) + + class Restart(cherrypy.process.plugins.Monitor): + def __init__(self, bus, frequency=1): + cherrypy.process.plugins.Monitor.__init__( + self, bus, self.run, frequency) + + def start(self): + cherrypy.process.plugins.Monitor.start(self) + + def run(self): + self.bus.log('Restarting.') + self.thread.cancel() + self.bus.restart() + + if restart: + restart = Restart(cherrypy.engine) + restart.subscribe() + restart.start() + return {'restarted': datetime.datetime.now(datetime.timezone.utc)} + return {'status': 'updated', 'time': datetime.datetime.now(datetime.timezone.utc)}
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image/rest/tiles.html b/_modules/girder_large_image/rest/tiles.html new file mode 100644 index 000000000..ba7b51e8b --- /dev/null +++ b/_modules/girder_large_image/rest/tiles.html @@ -0,0 +1,1742 @@ + + + + + + girder_large_image.rest.tiles — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image.rest.tiles

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import hashlib
+import io
+import math
+import os
+import pathlib
+import pickle
+import re
+import urllib
+
+import cherrypy
+
+import large_image
+from girder.api import access, filter_logging
+from girder.api.describe import Description, autoDescribeRoute, describeRoute
+from girder.api.rest import filtermodel, loadmodel, setRawResponse, setResponseHeader
+from girder.api.v1.item import Item as ItemResource
+from girder.constants import AccessType, TokenScope
+from girder.exceptions import RestException
+from girder.models.file import File
+from girder.models.item import Item
+from girder.models.upload import Upload
+from girder.utility.progress import setResponseTimeLimit
+from large_image.cache_util import strhash
+from large_image.constants import TileInputUnits, TileOutputMimeTypes
+from large_image.exceptions import TileGeneralError
+
+from .. import loadmodelcache
+from ..models.image_item import ImageItem
+
+MimeTypeExtensions = {
+    'image/jpeg': 'jpg',
+    'image/png': 'png',
+    'image/tiff': 'tiff',
+}
+for key, value in TileOutputMimeTypes.items():
+    if value not in MimeTypeExtensions:
+        MimeTypeExtensions[value] = key.lower()
+ImageMimeTypes = list(MimeTypeExtensions)
+EncodingTypes = list(TileOutputMimeTypes.keys()) + [
+    'pickle', 'pickle:3', 'pickle:4', 'pickle:5']
+
+
+def _adjustParams(params):
+    """
+    Check the user agent of a request.  If it appears to be from an iOS device,
+    and the request is asking for JPEG encoding (or hasn't specified an
+    encoding), then make sure the output is JFIF.
+
+    It is unfortunate that this requires analyzing the user agent, as this
+    method if brittle.  However, other browsers can handle non-JFIF jpegs, and
+    we do not want to encur the overhead of conversion if it is not necessary
+    (converting to JFIF may require colorspace transforms).
+
+    :param params: the request parameters.  May be modified.
+    """
+    try:
+        userAgent = cherrypy.request.headers.get('User-Agent', '').lower()
+    except Exception:
+        pass
+    if params.get('encoding', 'JPEG') == 'JPEG':
+        if ('ipad' in userAgent or 'ipod' in userAgent or 'iphone' in userAgent or
+                re.match('((?!chrome|android).)*safari', userAgent, re.IGNORECASE)):
+            params['encoding'] = 'JFIF'
+
+
+def _handleETag(key, item, *args, **kwargs):
+    """
+    Add or check an ETag header.
+
+    :param key: key for making a distinct etag.
+    :param item: item used for the item _id and updated timestamp.
+    :param max_age: the maximum cache duration.
+    :param *args, **kwargs: additional arguments for generating an etag.
+    """
+    max_age = kwargs.get('max_age', 3600)
+    id = str(item['_id'])
+    date = item.get('updated', item.get('created'))
+    etag = hashlib.md5(strhash(key, id, date, *args, **kwargs).encode()).hexdigest()
+    setResponseHeader('ETag', '"%s"' % etag)
+    conditions = [str(x) for x in cherrypy.request.headers.elements('If-Match') or []]
+    if conditions and not (conditions == ['*'] or etag in conditions):
+        raise cherrypy.HTTPError(
+            412, 'If-Match failed: ETag %r did not match %r' % (etag, conditions))
+    conditions = [str(x).strip('"')
+                  for x in cherrypy.request.headers.elements('If-None-Match') or []]
+    if conditions == ['*'] or etag in conditions:
+        raise cherrypy.HTTPRedirect([], 304)
+    # Explicitly set a max-age to recheck the cache after a while
+    setResponseHeader('Cache-control', f'public, max-age={max_age}')
+
+
+def _pickleParams(params):
+    """
+    Check if the output should be returned as pickled data and adjust the
+    parameters accordingly.
+
+    :param params: a dictionary of parameters.  If encoding starts with
+        'pickle', numpy format will be requested.
+    :return: None if the output should not be pickled.  Otherwise, the pickle
+        protocol that should be used.
+    """
+    if not str(params.get('encoding')).startswith('pickle'):
+        return None
+    params['format'] = large_image.constants.TILE_FORMAT_NUMPY
+    pickle = params['encoding'].split(':')[-1]
+    del params['encoding']
+    return int(pickle) or 4 if pickle.isdigit() else 4
+
+
+def _pickleOutput(data, protocol):
+    """
+    Pickle some data using a specific protocol and return the pickled data
+    and the recommended mime type.
+
+    :param data: the data to pickle.
+    :param protocol: the pickle protocol level.
+    :returns: the pickled data and the mime type.
+    """
+    return (
+        pickle.dumps(data, protocol=min(protocol, pickle.HIGHEST_PROTOCOL)),
+        'application/octet-stream')
+
+
+
+[docs] +class TilesItemResource(ItemResource): + + def __init__(self, apiRoot): + # Don't call the parent (Item) constructor, to avoid redefining routes, + # but do call the grandparent (Resource) constructor + super(ItemResource, self).__init__() + + self.resourceName = 'item' + apiRoot.item.route('POST', (':itemId', 'tiles'), self.createTiles) + apiRoot.item.route('POST', (':itemId', 'tiles', 'convert'), self.convertImage) + apiRoot.item.route('GET', (':itemId', 'tiles'), self.getTilesInfo) + apiRoot.item.route('DELETE', (':itemId', 'tiles'), self.deleteTiles) + apiRoot.item.route('GET', (':itemId', 'tiles', 'thumbnail'), self.getTilesThumbnail) + apiRoot.item.route('GET', (':itemId', 'tiles', 'thumbnails'), self.listTilesThumbnails) + apiRoot.item.route('DELETE', (':itemId', 'tiles', 'thumbnails'), self.deleteTilesThumbnails) + apiRoot.item.route('POST', (':itemId', 'tiles', 'thumbnails'), self.addTilesThumbnails) + apiRoot.item.route('GET', (':itemId', 'tiles', 'region'), self.getTilesRegion) + apiRoot.item.route('GET', (':itemId', 'tiles', 'tile_frames'), self.tileFrames) + apiRoot.item.route('GET', (':itemId', 'tiles', 'tile_frames', 'quad_info'), + self.tileFramesQuadInfo) + apiRoot.item.route('GET', (':itemId', 'tiles', 'pixel'), self.getTilesPixel) + apiRoot.item.route('GET', (':itemId', 'tiles', 'histogram'), self.getHistogram) + apiRoot.item.route('GET', (':itemId', 'tiles', 'bands'), self.getBandInformation) + apiRoot.item.route('GET', (':itemId', 'tiles', 'zxy', ':z', ':x', ':y'), self.getTile) + apiRoot.item.route('GET', (':itemId', 'tiles', 'fzxy', ':frame', ':z', ':x', ':y'), + self.getTileWithFrame) + apiRoot.item.route('GET', (':itemId', 'tiles', 'images'), self.getAssociatedImagesList) + apiRoot.item.route('GET', (':itemId', 'tiles', 'images', ':image'), + self.getAssociatedImage) + apiRoot.item.route('GET', (':itemId', 'tiles', 'images', ':image', 'metadata'), + self.getAssociatedImageMetadata) + apiRoot.item.route('GET', ('test', 'tiles'), self.getTestTilesInfo) + apiRoot.item.route('GET', ('test', 'tiles', 'zxy', ':z', ':x', ':y'), self.getTestTile) + apiRoot.item.route('GET', (':itemId', 'tiles', 'dzi.dzi'), self.getDZIInfo) + apiRoot.item.route('GET', (':itemId', 'tiles', 'dzi_files', ':level', ':xandy'), + self.getDZITile) + apiRoot.item.route('GET', (':itemId', 'tiles', 'internal_metadata'), + self.getInternalMetadata) + # Logging rate limiters + filter_logging.addLoggingFilter( + 'GET (/[^/ ?#]+)*/item/[^/ ?#]+/tiles/zxy(/[^/ ?#]+){3}', + frequency=250, duration=10) + filter_logging.addLoggingFilter( + 'GET (/[^/ ?#]+)*/item/[^/ ?#]+/tiles/fzxy(/[^/ ?#]+){3}', + frequency=250, duration=10) + filter_logging.addLoggingFilter( + 'GET (/[^/ ?#]+)*/item/[^/ ?#]+/tiles/dzi_files(/[^/ ?#]+){2}', + frequency=250, duration=10) + filter_logging.addLoggingFilter( + 'GET (/[^/ ?#]+)*/item/[^/ ?#]+/tiles/region', + frequency=100, duration=10) + # Cache the model singleton + self.imageItemModel = ImageItem() + +
+[docs] + @describeRoute( + Description('Create a large image for this item.') + .param('itemId', 'The source item.', paramType='path') + .param('fileId', 'The source file containing the image. Required if ' + 'there is more than one file in the item.', required=False) + .param('force', 'Always use a job to create the large image.', + dataType='boolean', default=False, required=False) + .param('notify', 'If a job is required to create the large image, ' + 'a nofication can be sent when it is complete.', + dataType='boolean', default=True, required=False) + .param('localJob', 'If true, run as a local job; if false, run via ' + 'the remote worker', dataType='boolean', required=False) + .param('tileSize', 'Tile size', dataType='int', default=256, + required=False) + .param('compression', 'Internal compression format', required=False, + enum=['none', 'jpeg', 'deflate', 'lzw', 'zstd', 'packbits', 'webp', 'jp2k']) + .param('quality', 'JPEG compression quality where 0 is small and 100 ' + 'is highest quality', dataType='int', default=90, + required=False) + .param('level', 'Compression level for deflate (zip) or zstd.', + dataType='int', required=False) + .param('predictor', 'Predictor for deflate (zip) or lzw.', + required=False, enum=['none', 'horizontal', 'float', 'yes']) + .param('psnr', 'JP2K compression target peak-signal-to-noise-ratio ' + 'where 0 is lossless and otherwise higher numbers are higher ' + 'quality', dataType='int', required=False) + .param('cr', 'JP2K target compression ratio where 1 is lossless', + dataType='int', required=False) + .param('concurrent', 'Suggested number of maximum concurrent ' + 'processes to use during conversion. Values less than or ' + 'equal to 0 use the number of logical cpus less that value. ' + 'Default is -2.', dataType='int', required=False), + ) + @access.user(scope=TokenScope.DATA_WRITE) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.WRITE) + @filtermodel(model='job', plugin='jobs') + def createTiles(self, item, params): + if 'concurrent' in params: + params['_concurrency'] = params.pop('concurrent') + largeImageFileId = params.get('fileId') + if largeImageFileId is None: + files = list(Item().childFiles(item=item, limit=2)) + if len(files) == 1: + largeImageFileId = str(files[0]['_id']) + if not largeImageFileId: + msg = 'Missing "fileId" parameter.' + raise RestException(msg) + largeImageFile = File().load(largeImageFileId, force=True, exc=True) + user = self.getCurrentUser() + token = self.getCurrentToken() + notify = self.boolParam('notify', params, default=True) + params.pop('notify', None) + localJob = self.boolParam('localJob', params, default=None) + params.pop('localJob', None) + try: + return self.imageItemModel.createImageItem( + item, largeImageFile, user, token, + createJob='always' if self.boolParam('force', params, default=False) else True, + notify=notify, + localJob=localJob, + **params) + except TileGeneralError as e: + raise RestException(e.args[0])
+ + +
+[docs] + @describeRoute( + Description('Create a new large image item based on an existing item') + .notes('This can be used to make an item that is a different internal ' + 'format than the original item.') + .param('itemId', 'The source item.', paramType='path') + .param('fileId', 'The source file containing the image. Required if ' + 'there is more than one file in the item.', required=False) + .param('folderId', 'The destination folder.', required=False) + .param('name', 'A new name for the output item.', required=False) + .param('localJob', 'If true, run as a local job; if false, run via ' + 'the remote worker', dataType='boolean', required=False) + .param('tileSize', 'Tile size', dataType='int', default=256, + required=False) + .param('onlyFrame', 'Only convert a specific 0-based frame of a ' + 'multiframe file. If not specified, all frames are converted.', + dataType='int', required=False) + .param('format', 'File format', required=False, + enum=['tiff', 'aperio']) + .param('compression', 'Internal compression format', required=False, + enum=['none', 'jpeg', 'deflate', 'lzw', 'zstd', 'packbits', 'webp', 'jp2k']) + .param('quality', 'JPEG compression quality where 0 is small and 100 ' + 'is highest quality', dataType='int', default=90, + required=False) + .param('level', 'Compression level for deflate (zip) or zstd.', + dataType='int', required=False) + .param('predictor', 'Predictor for deflate (zip) or lzw.', + required=False, enum=['none', 'horizontal', 'float', 'yes']) + .param('psnr', 'JP2K compression target peak-signal-to-noise-ratio ' + 'where 0 is lossless and otherwise higher numbers are higher ' + 'quality', dataType='int', required=False) + .param('cr', 'JP2K target compression ratio where 1 is lossless', + dataType='int', required=False) + .param('concurrent', 'Suggested number of maximum concurrent ' + 'processes to use during conversion. Values less than or ' + 'equal to 0 use the number of logical cpus less that value. ' + 'Default is -2.', dataType='int', required=False), + ) + @access.user(scope=TokenScope.DATA_WRITE) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + @filtermodel(model='job', plugin='jobs') + def convertImage(self, item, params): + if 'concurrent' in params: + params['_concurrency'] = params.pop('concurrent') + largeImageFileId = params.get('fileId') + if largeImageFileId is None: + files = list(Item().childFiles(item=item, limit=2)) + if len(files) == 1: + largeImageFileId = str(files[0]['_id']) + if not largeImageFileId: + msg = 'Missing "fileId" parameter.' + raise RestException(msg) + largeImageFile = File().load(largeImageFileId, force=True, exc=True) + user = self.getCurrentUser() + token = self.getCurrentToken() + params.pop('notify', None) + localJob = self.boolParam('localJob', params, default=True) + params.pop('localJob', None) + try: + return self.imageItemModel.convertImage( + item, largeImageFile, user, token, localJob=localJob, **params) + except TileGeneralError as e: + raise RestException(e.args[0])
+ + + @classmethod + def _parseTestParams(cls, params): + _adjustParams(params) + return cls._parseParams(params, False, [ + ('minLevel', int), + ('maxLevel', int), + ('tileWidth', int), + ('tileHeight', int), + ('sizeX', int), + ('sizeY', int), + ('fractal', lambda val: val == 'true'), + ('frame', int), + ('frames', str), + ('monochrome', lambda val: val == 'true'), + ('encoding', str), + ]) + + @classmethod + def _parseParams(cls, params, keepUnknownParams, typeList): + """ + Given a dictionary of parameters, check that a list of parameters are + valid data types. The parameters within the list are validated and + copied to a dictionary by themselves. + + :param params: the dictionary of parameters to validate. + :param keepUnknownParams: True to copy all parameters, not just those + in the typeList. The parameters in the typeList are still + validated. + :param typeList: a list of tuples of the form (key, dataType, [outkey1, + [outkey2]]). If output keys are used, the original key is renamed + to the the output key. If two output keys are specified, the + original key is renamed to outkey2 and placed in a sub-dictionary + names outkey1. + :returns: params: a validated and possibly filtered list of parameters. + """ + results = {} + if keepUnknownParams: + results = dict(params) + for entry in typeList: + key, dataType, outkey1, outkey2 = (list(entry) + [None] * 2)[:4] + if key in params: + if dataType == 'boolOrInt': + dataType = bool if str(params[key]).lower() in ( + 'true', 'false', 'on', 'off', 'yes', 'no') else int + try: + if dataType is bool: + results[key] = str(params[key]).lower() in ( + 'true', 'on', 'yes', '1') + else: + results[key] = dataType(params[key]) + except ValueError: + raise RestException( + '"%s" parameter is an incorrect type.' % key) + if outkey1 is not None: + if outkey2 is not None: + results.setdefault(outkey1, {})[outkey2] = results[key] + else: + results[outkey1] = results[key] + del results[key] + return results + + def _getTilesInfo(self, item, imageArgs): + """ + Get metadata for an item's large image. + + :param item: the item to query. + :param imageArgs: additional arguments to use when fetching image data. + :return: the tile metadata. + """ + try: + return self.imageItemModel.getMetadata(item, **imageArgs) + except TileGeneralError as e: + raise RestException(e.args[0], code=400) + + def _setContentDisposition(self, item, contentDisposition, mime, subname, fullFilename=None): + """ + If requested, set the content disposition and a suggested file name. + + :param item: an item that includes a name. + :param contentDisposition: either 'inline' or 'attachment', otherwise + no header is added. + :param mime: the mimetype of the output image. Used for the filename + suffix. + :param subname: a subname to append to the item name. + :param fullFilename: if specified, use this instead of the item name + and the subname. + """ + if (not item or not item.get('name') or + mime not in MimeTypeExtensions or + contentDisposition not in ('inline', 'attachment')): + return + if fullFilename: + filename = fullFilename + else: + filename = os.path.splitext(item['name'])[0] + if subname: + filename += '-' + subname + filename += '.' + MimeTypeExtensions[mime] + if not isinstance(filename, str): + filename = filename.decode('utf8', 'ignore') + safeFilename = filename.encode('ascii', 'ignore').replace(b'"', b'') + encodedFilename = urllib.parse.quote(filename.encode('utf8', 'ignore')) + setResponseHeader( + 'Content-Disposition', + '%s; filename="%s"; filename*=UTF-8\'\'%s' % ( + contentDisposition, safeFilename, encodedFilename)) + +
+[docs] + @describeRoute( + Description('Get large image metadata.') + .param('itemId', 'The ID of the item.', paramType='path') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getTilesInfo(self, item, params): + return self._getTilesInfo(item, params)
+ + +
+[docs] + @describeRoute( + Description('Get large image internal metadata.') + .param('itemId', 'The ID of the item.', paramType='path') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getInternalMetadata(self, item, params): + try: + return self.imageItemModel.getInternalMetadata(item, **params) + except TileGeneralError as e: + raise RestException(e.args[0], code=400)
+ + +
+[docs] + @describeRoute( + Description('Get test large image metadata.'), + ) + @access.public(scope=TokenScope.DATA_READ) + def getTestTilesInfo(self, params): + item = {'largeImage': {'sourceName': 'test'}} + imageArgs = self._parseTestParams(params) + return self._getTilesInfo(item, imageArgs)
+ + +
+[docs] + @describeRoute( + Description('Get DeepZoom compatible metadata.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('overlap', 'Pixel overlap (default 0), must be non-negative.', + required=False, dataType='int') + .param('tilesize', 'Tile size (default 256), must be a power of 2', + required=False, dataType='int') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getDZIInfo(self, item, params): + if 'encoding' in params and params['encoding'] not in ('JPEG', 'PNG'): + msg = 'Only JPEG and PNG encodings are supported' + raise RestException(msg, code=400) + info = self._getTilesInfo(item, params) + tilesize = int(params.get('tilesize', 256)) + if tilesize & (tilesize - 1): + msg = 'Invalid tilesize' + raise RestException(msg, code=400) + overlap = int(params.get('overlap', 0)) + if overlap < 0: + msg = 'Invalid overlap' + raise RestException(msg, code=400) + result = ''.join([ + '<?xml version="1.0" encoding="UTF-8"?>', + '<Image', + ' TileSize="%d"' % tilesize, + ' Overlap="%d"' % overlap, + ' Format="%s"' % ('png' if params.get('encoding') == 'PNG' else 'jpg'), + ' xmlns="http://schemas.microsoft.com/deepzoom/2008">', + '<Size', + ' Width="%d"' % info['sizeX'], + ' Height="%d"' % info['sizeY'], + '/>' + '</Image>', + ]) + setResponseHeader('Content-Type', 'text/xml') + setRawResponse() + return result
+ + + def _getTile(self, item, z, x, y, imageArgs, mayRedirect=False): + """ + Get an large image tile. + + :param item: the item to get a tile from. + :param z: tile layer number (0 is the most zoomed-out). + .param x: the X coordinate of the tile (0 is the left side). + .param y: the Y coordinate of the tile (0 is the top). + :param imageArgs: additional arguments to use when fetching image data. + :param mayRedirect: if True or one of 'any', 'encoding', or 'exact', + allow return a response which may be a redirect. + :return: a function that returns the raw image data. + """ + try: + x, y, z = int(x), int(y), int(z) + except ValueError: + msg = 'x, y, and z must be integers' + raise RestException(msg, code=400) + if x < 0 or y < 0 or z < 0: + msg = 'x, y, and z must be positive integers' + raise RestException(msg, + code=400) + result = self.imageItemModel._tileFromHash( + item, x, y, z, mayRedirect=mayRedirect, **imageArgs) + if result is not None: + tileData, tileMime = result + else: + try: + tileData, tileMime = self.imageItemModel.getTile( + item, x, y, z, mayRedirect=mayRedirect, **imageArgs) + except TileGeneralError as e: + raise RestException(e.args[0], code=404) + setResponseHeader('Content-Type', tileMime) + setRawResponse() + return tileData + +
+[docs] + @describeRoute( + Description('Get a large image tile.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('z', 'The layer number of the tile (0 is the most zoomed-out ' + 'layer).', paramType='path') + .param('x', 'The X coordinate of the tile (0 is the left side).', + paramType='path') + .param('y', 'The Y coordinate of the tile (0 is the top).', + paramType='path') + .param('redirect', 'If the tile exists as a complete file, allow an ' + 'HTTP redirect instead of returning the data directly. The ' + 'redirect might not have the correct mime type. "exact" must ' + 'match the image encoding and quality parameters, "encoding" ' + 'must match the image encoding but disregards quality, and ' + '"any" will redirect to any image if possible.', required=False, + enum=['false', 'exact', 'encoding', 'any'], default='false') + .produces(ImageMimeTypes) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + # Without caching, this checks for permissions every time. By using the + # LoadModelCache, three database lookups are avoided, which saves around + # 6 ms in tests. We also avoid the @access.public decorator and directly + # set the accessLevel attribute on the method. + # @access.public(cookie=True, scope=TokenScope.DATA_READ) + # @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + # def getTile(self, item, z, x, y, params): + # return self._getTile(item, z, x, y, params, True) + def getTile(self, itemId, z, x, y, params): + _adjustParams(params) + item = loadmodelcache.loadModel( + self, 'item', id=itemId, allowCookie=True, level=AccessType.READ) + _handleETag('getTile', item, z, x, y, params) + redirect = params.get('redirect', False) + if redirect not in ('any', 'exact', 'encoding'): + redirect = False + return self._getTile(item, z, x, y, params, mayRedirect=redirect)
+ + getTile.accessLevel = 'public' + getTile.cookieAuth = True + getTile.requiredScopes = TokenScope.DATA_READ + +
+[docs] + @describeRoute( + Description('Get a large image tile with a frame number.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('frame', 'The frame number of the tile.', paramType='path') + .param('z', 'The layer number of the tile (0 is the most zoomed-out ' + 'layer).', paramType='path') + .param('x', 'The X coordinate of the tile (0 is the left side).', + paramType='path') + .param('y', 'The Y coordinate of the tile (0 is the top).', + paramType='path') + .param('redirect', 'If the tile exists as a complete file, allow an ' + 'HTTP redirect instead of returning the data directly. The ' + 'redirect might not have the correct mime type. "exact" must ' + 'match the image encoding and quality parameters, "encoding" ' + 'must match the image encoding but disregards quality, and ' + '"any" will redirect to any image if possible.', required=False, + enum=['false', 'exact', 'encoding', 'any'], default='false') + .produces(ImageMimeTypes) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + # See getTile for caching rationale + def getTileWithFrame(self, itemId, frame, z, x, y, params): + _adjustParams(params) + item = loadmodelcache.loadModel( + self, 'item', id=itemId, allowCookie=True, level=AccessType.READ) + _handleETag('getTileWithFrame', item, frame, z, x, y, params) + redirect = params.get('redirect', False) + if redirect not in ('any', 'exact', 'encoding'): + redirect = False + params['frame'] = frame + return self._getTile(item, z, x, y, params, mayRedirect=redirect)
+ + getTileWithFrame.accessLevel = 'public' + +
+[docs] + @describeRoute( + Description('Get a test large image tile.') + .param('z', 'The layer number of the tile (0 is the most zoomed-out ' + 'layer).', paramType='path') + .param('x', 'The X coordinate of the tile (0 is the left side).', + paramType='path') + .param('y', 'The Y coordinate of the tile (0 is the top).', + paramType='path') + .produces(ImageMimeTypes), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + def getTestTile(self, z, x, y, params): + item = {'largeImage': {'sourceName': 'test'}} + imageArgs = self._parseTestParams(params) + return self._getTile(item, z, x, y, imageArgs)
+ + +
+[docs] + @describeRoute( + Description('Get a DeepZoom image tile.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('level', 'The deepzoom layer number of the tile (8 is the ' + 'most zoomed-out layer).', paramType='path') + .param('xandy', 'The X and Y coordinate of the tile in the form ' + '(x)_(y).(extension) where (0_0 is the left top).', + paramType='path') + .produces(ImageMimeTypes) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getDZITile(self, item, level, xandy, params): + _adjustParams(params) + tilesize = int(params.get('tilesize', 256)) + if tilesize & (tilesize - 1): + msg = 'Invalid tilesize' + raise RestException(msg, code=400) + overlap = int(params.get('overlap', 0)) + if overlap < 0: + msg = 'Invalid overlap' + raise RestException(msg, code=400) + x, y = (int(xy) for xy in xandy.split('.')[0].split('_')) + _handleETag('getDZITile', item, level, xandy, params) + metadata = self.imageItemModel.getMetadata(item, **params) + level = int(level) + maxlevel = int(math.ceil(math.log(max( + metadata['sizeX'], metadata['sizeY'])) / math.log(2))) + if level < 1 or level > maxlevel: + msg = 'level must be between 1 and the image scale' + raise RestException(msg, + code=400) + lfactor = 2 ** (maxlevel - level) + region = { + 'left': (x * tilesize - overlap) * lfactor, + 'top': (y * tilesize - overlap) * lfactor, + 'right': ((x + 1) * tilesize + overlap) * lfactor, + 'bottom': ((y + 1) * tilesize + overlap) * lfactor, + } + width = height = tilesize + overlap * 2 + if region['left'] < 0: + width += int(region['left'] / lfactor) + region['left'] = 0 + if region['top'] < 0: + height += int(region['top'] / lfactor) + region['top'] = 0 + if region['left'] >= metadata['sizeX']: + msg = 'x is outside layer' + raise RestException(msg, code=400) + if region['top'] >= metadata['sizeY']: + msg = 'y is outside layer' + raise RestException(msg, code=400) + if region['left'] < metadata['sizeX'] and region['right'] > metadata['sizeX']: + region['right'] = metadata['sizeX'] + width = int(math.ceil(float(region['right'] - region['left']) / lfactor)) + if region['top'] < metadata['sizeY'] and region['bottom'] > metadata['sizeY']: + region['bottom'] = metadata['sizeY'] + height = int(math.ceil(float(region['bottom'] - region['top']) / lfactor)) + regionData, regionMime = self.imageItemModel.getRegion( + item, + region=region, + output=dict(maxWidth=width, maxHeight=height), + **params) + setResponseHeader('Content-Type', regionMime) + setRawResponse() + return regionData
+ + +
+[docs] + @describeRoute( + Description('Remove a large image from this item.') + .param('itemId', 'The ID of the item.', paramType='path'), + ) + @access.user(scope=TokenScope.DATA_WRITE) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.WRITE) + def deleteTiles(self, item, params): + deleted = self.imageItemModel.delete(item) + return { + 'deleted': deleted, + }
+ + +
+[docs] + @describeRoute( + Description('Get a thumbnail of a large image item.') + .notes('Aspect ratio is always preserved. If both width and height ' + 'are specified, the resulting thumbnail may be smaller in one ' + 'of the two dimensions. If neither width nor height is given, ' + 'a default size will be returned. ' + 'This creates a thumbnail from the lowest level of the source ' + 'image, which means that asking for a large thumbnail will not ' + 'be a high-quality image.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('width', 'The maximum width of the thumbnail in pixels.', + required=False, dataType='int') + .param('height', 'The maximum height of the thumbnail in pixels.', + required=False, dataType='int') + .param('fill', 'A fill color. If width and height are both specified ' + 'and fill is specified and not "none", the output image is ' + 'padded on either the sides or the top and bottom to the ' + 'requested output size. Most css colors are accepted.', + required=False) + .param('frame', 'For multiframe images, the 0-based frame number. ' + 'This is ignored on non-multiframe images.', required=False, + dataType='int') + .param('encoding', 'Output image encoding. TILED generates a tiled ' + 'tiff without the upper limit on image size the other options ' + 'have. For geospatial sources, TILED will also have ' + 'appropriate tagging. Pickle emits python pickle data with an ' + 'optional specific protocol', required=False, + enum=EncodingTypes, default='JPEG') + .param('contentDisposition', 'Specify the Content-Disposition response ' + 'header disposition-type value.', required=False, + enum=['inline', 'attachment']) + .param('contentDispositionFilename', 'Specify the filename used in ' + 'the Content-Disposition response header.', required=False) + .produces(ImageMimeTypes) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getTilesThumbnail(self, item, params): + _adjustParams(params) + params = self._parseParams(params, True, [ + ('width', int), + ('height', int), + ('fill', str), + ('frame', int), + ('jpegQuality', int), + ('jpegSubsampling', int), + ('tiffCompression', str), + ('encoding', str), + ('style', str), + ('contentDisposition', str), + ('contentDispositionFileName', str), + ]) + _handleETag('getTilesThumbnail', item, params) + pickle = _pickleParams(params) + try: + result = self.imageItemModel.getThumbnail(item, **params) + except TileGeneralError as e: + raise RestException(e.args[0]) + except ValueError as e: + raise RestException('Value Error: %s' % e.args[0]) + if not isinstance(result, tuple): + return result + thumbData, thumbMime = result + if pickle: + thumbData, thumbMime = _pickleOutput(thumbData, pickle) + self._setContentDisposition( + item, params.get('contentDisposition'), thumbMime, 'thumbnail', + params.get('contentDispositionFilename')) + setResponseHeader('Content-Type', thumbMime) + setRawResponse() + return thumbData
+ + +
+[docs] + @describeRoute( + Description('Get any region of a large image item, optionally scaling ' + 'it.') + .notes('If neither width nor height is specified, the full resolution ' + 'region is returned. If a width or height is specified, ' + 'aspect ratio is always preserved (if both are given, the ' + 'resulting image may be smaller in one of the two ' + 'dimensions). When scaling must be applied, the image is ' + 'downsampled from a higher resolution layer, never upsampled.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('left', 'The left column (0-based) of the region to process. ' + 'Negative values are offsets from the right edge.', + required=False, dataType='float') + .param('top', 'The top row (0-based) of the region to process. ' + 'Negative values are offsets from the bottom edge.', + required=False, dataType='float') + .param('right', 'The right column (0-based from the left) of the ' + 'region to process. The region will not include this column. ' + 'Negative values are offsets from the right edge.', + required=False, dataType='float') + .param('bottom', 'The bottom row (0-based from the top) of the region ' + 'to process. The region will not include this row. Negative ' + 'values are offsets from the bottom edge.', + required=False, dataType='float') + .param('regionWidth', 'The width of the region to process.', + required=False, dataType='float') + .param('regionHeight', 'The height of the region to process.', + required=False, dataType='float') + .param('units', 'Units used for left, top, right, bottom, ' + 'regionWidth, and regionHeight. base_pixels are pixels at the ' + 'maximum resolution, pixels and mm are at the specified ' + 'magnfication, fraction is a scale of [0-1].', required=False, + enum=sorted(set(TileInputUnits.values())), + default='base_pixels') + + .param('width', 'The maximum width of the output image in pixels.', + required=False, dataType='int') + .param('height', 'The maximum height of the output image in pixels.', + required=False, dataType='int') + .param('fill', 'A fill color. If output dimensions are specified and ' + 'fill is specified and not "none", the output image is padded ' + 'on either the sides or the top and bottom to the requested ' + 'output size. Most css colors are accepted.', required=False) + .param('magnification', 'Magnification of the output image. If ' + 'neither width for height is specified, the magnification, ' + 'mm_x, and mm_y parameters are used to select the output size.', + required=False, dataType='float') + .param('mm_x', 'The size of the output pixels in millimeters', + required=False, dataType='float') + .param('mm_y', 'The size of the output pixels in millimeters', + required=False, dataType='float') + .param('exact', 'If magnification, mm_x, or mm_y are specified, they ' + 'must match an existing level of the image exactly.', + required=False, dataType='boolean', default=False) + .param('frame', 'For multiframe images, the 0-based frame number. ' + 'This is ignored on non-multiframe images.', required=False, + dataType='int') + .param('encoding', 'Output image encoding. TILED generates a tiled ' + 'tiff without the upper limit on image size the other options ' + 'have. For geospatial sources, TILED will also have ' + 'appropriate tagging. Pickle emits python pickle data with an ' + 'optional specific protocol', required=False, + enum=EncodingTypes, default='JPEG') + .param('jpegQuality', 'Quality used for generating JPEG images', + required=False, dataType='int', default=95) + .param('jpegSubsampling', 'Chroma subsampling used for generating ' + 'JPEG images. 0, 1, and 2 are full, half, and quarter ' + 'resolution chroma respectively.', required=False, + enum=['0', '1', '2'], dataType='int', default='0') + .param('tiffCompression', 'Compression method when storing a TIFF ' + 'image', required=False, + enum=['none', 'raw', 'lzw', 'tiff_lzw', 'jpeg', 'deflate', + 'tiff_adobe_deflate']) + .param('style', 'JSON-encoded style string', required=False) + .param('resample', 'If false, an existing level of the image is used ' + 'for the region. If true, the internal values are ' + 'interpolated to match the specified size as needed. 0-3 for ' + 'a specific interpolation method (0-nearest, 1-lanczos, ' + '2-bilinear, 3-bicubic)', required=False, + enum=['false', 'true', '0', '1', '2', '3']) + .param('contentDisposition', 'Specify the Content-Disposition response ' + 'header disposition-type value.', required=False, + enum=['inline', 'attachment']) + .param('contentDispositionFilename', 'Specify the filename used in ' + 'the Content-Disposition response header.', required=False) + .produces(ImageMimeTypes) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403) + .errorResponse('Insufficient memory.'), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getTilesRegion(self, item, params): + _adjustParams(params) + params = self._parseParams(params, True, [ + ('left', float, 'region', 'left'), + ('top', float, 'region', 'top'), + ('right', float, 'region', 'right'), + ('bottom', float, 'region', 'bottom'), + ('regionWidth', float, 'region', 'width'), + ('regionHeight', float, 'region', 'height'), + ('units', str, 'region', 'units'), + ('unitsWH', str, 'region', 'unitsWH'), + ('width', int, 'output', 'maxWidth'), + ('height', int, 'output', 'maxHeight'), + ('fill', str), + ('magnification', float, 'scale', 'magnification'), + ('mm_x', float, 'scale', 'mm_x'), + ('mm_y', float, 'scale', 'mm_y'), + ('exact', bool, 'scale', 'exact'), + ('frame', int), + ('encoding', str), + ('jpegQuality', int), + ('jpegSubsampling', int), + ('tiffCompression', str), + ('style', str), + ('resample', 'boolOrInt'), + ('contentDisposition', str), + ('contentDispositionFileName', str), + ]) + _handleETag('getTilesRegion', item, params) + pickle = _pickleParams(params) + setResponseTimeLimit(86400) + try: + regionData, regionMime = self.imageItemModel.getRegion( + item, **params) + if pickle: + regionData, regionMime = _pickleOutput(regionData, pickle) + except TileGeneralError as e: + raise RestException(e.args[0]) + except ValueError as e: + raise RestException('Value Error: %s' % e.args[0]) + self._setContentDisposition( + item, params.get('contentDisposition'), regionMime, 'region', + params.get('contentDispositionFilename')) + setResponseHeader('Content-Type', regionMime) + if isinstance(regionData, pathlib.Path): + BUF_SIZE = 65536 + + def stream(): + try: + with regionData.open('rb') as f: + while True: + data = f.read(BUF_SIZE) + if not data: + break + yield data + finally: + regionData.unlink() + return stream + setRawResponse() + return regionData
+ + +
+[docs] + @describeRoute( + Description('Get a single pixel of a large image item.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('left', 'The left column (0-based) of the pixel.', + required=False, dataType='float') + .param('top', 'The top row (0-based) of the pixel.', + required=False, dataType='float') + .param('units', 'Units used for left and top. base_pixels are pixels ' + 'at the maximum resolution, pixels and mm are at the specified ' + 'magnfication, fraction is a scale of [0-1].', required=False, + enum=sorted(set(TileInputUnits.values())), + default='base_pixels') + .param('frame', 'For multiframe images, the 0-based frame number. ' + 'This is ignored on non-multiframe images.', required=False, + dataType='int') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getTilesPixel(self, item, params): + params = self._parseParams(params, True, [ + ('left', float, 'region', 'left'), + ('top', float, 'region', 'top'), + ('right', float, 'region', 'right'), + ('bottom', float, 'region', 'bottom'), + ('units', str, 'region', 'units'), + ('frame', int), + ]) + try: + pixel = self.imageItemModel.getPixel(item, **params) + except TileGeneralError as e: + raise RestException(e.args[0]) + except ValueError as e: + raise RestException('Value Error: %s' % e.args[0]) + return pixel
+ + + def _cacheHistograms(self, item, histRange, cache, params): + needed = [] + result = {'cached': []} + tilesource = self.imageItemModel._loadTileSource(item, **params) + for frame in range(tilesource.frames): + if histRange is not None and histRange != 'round': + continue + checkParams = params.copy() + checkParams['range'] = histRange + if tilesource.frames > 1: + checkParams['frame'] = frame + else: + checkParams.pop('frame', None) + result['cached'].append(self.imageItemModel.histogram( + item, checkAndCreate='check', **checkParams)) + if not result['cached'][-1]: + needed.append(checkParams) + if cache == 'schedule' and not all(result['cached']): + result['scheduledJob'] = str(self.imageItemModel._scheduleHistograms( + item, needed, self.getCurrentUser())['_id']) + return result + +
+[docs] + @describeRoute( + Description('Get a histogram for any region of a large image item.') + .notes('This can take all of the parameters as the region endpoint, ' + 'plus some histogram-specific parameters. Only typically used ' + 'parameters are listed. The returned result is a list with ' + 'one entry per channel (always one of L, LA, RGB, or RGBA ' + 'colorspace). Each entry has the histogram values, bin edges, ' + 'minimum and maximum values for the channel, and number of ' + 'samples (pixels) used in the computation.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('width', 'The maximum width of the analyzed region in pixels.', + default=2048, required=False, dataType='int') + .param('height', 'The maximum height of the analyzed region in pixels.', + default=2048, required=False, dataType='int') + .param('resample', 'If false, an existing level of the image is used ' + 'for the histogram. If true, the internal values are ' + 'interpolated to match the specified size as needed. 0-3 for ' + 'a specific interpolation method (0-nearest, 1-lanczos, ' + '2-bilinear, 3-bicubic)', required=False, + enum=['false', 'true', '0', '1', '2', '3'], default='false') + .param('frame', 'For multiframe images, the 0-based frame number. ' + 'This is ignored on non-multiframe images.', required=False, + dataType='int') + .param('bins', 'The number of bins in the histogram.', + default=256, required=False, dataType='int') + .param('rangeMin', 'The minimum value in the histogram. Defaults to ' + 'the minimum value in the image.', + required=False, dataType='float') + .param('rangeMax', 'The maximum value in the histogram. Defaults to ' + 'the maximum value in the image.', + required=False, dataType='float') + .param('roundRange', 'If true and neither a minimum or maximum is ' + 'specified for the range, round the bin edges and adjust the ' + 'number of bins for integer data with smaller ranges.', + required=False, dataType='boolean', default=False) + .param('density', 'If true, scale the results by the number of ' + 'samples.', required=False, dataType='boolean', default=False) + .param('cache', 'Report on or request caching the specified histogram ' + 'for all frames. Scheduling creates a local job.', + required=False, + enum=['none', 'report', 'schedule']) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getHistogram(self, item, params): + _adjustParams(params) + params = self._parseParams(params, True, [ + ('left', float, 'region', 'left'), + ('top', float, 'region', 'top'), + ('right', float, 'region', 'right'), + ('bottom', float, 'region', 'bottom'), + ('regionWidth', float, 'region', 'width'), + ('regionHeight', float, 'region', 'height'), + ('units', str, 'region', 'units'), + ('unitsWH', str, 'region', 'unitsWH'), + ('width', int, 'output', 'maxWidth'), + ('height', int, 'output', 'maxHeight'), + ('fill', str), + ('magnification', float, 'scale', 'magnification'), + ('mm_x', float, 'scale', 'mm_x'), + ('mm_y', float, 'scale', 'mm_y'), + ('exact', bool, 'scale', 'exact'), + ('frame', int), + ('encoding', str), + ('jpegQuality', int), + ('jpegSubsampling', int), + ('tiffCompression', str), + ('style', str), + ('resample', 'boolOrInt'), + ('bins', int), + ('rangeMin', int), + ('rangeMax', int), + ('roundRange', bool), + ('density', bool), + ]) + _handleETag('getHistogram', item, params) + histRange = None + if 'rangeMin' in params or 'rangeMax' in params: + histRange = [params.pop('rangeMin', 0), params.pop('rangeMax', 256)] + if params.get('roundRange'): + if params.pop('roundRange', False) and histRange is None: + histRange = 'round' + + cache = params.pop('cache', None) + if cache in {'report', 'schedule'}: + return self._cacheHistograms(item, histRange, cache, params) + result = self.imageItemModel.histogram(item, range=histRange, **params) + result = result['histogram'] + # Cast everything to lists and floats so json will encode properly + for entry in result: + for key in {'bin_edges', 'hist', 'range'}: + if key in entry: + entry[key] = [float(val) for val in list(entry[key])] + for key in {'min', 'max', 'samples'}: + if key in entry: + entry[key] = float(entry[key]) + return result
+ + +
+[docs] + @describeRoute( + Description('Get band information for a large image item.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('frame', 'For multiframe images, the 0-based frame number. ' + 'This is ignored on non-multiframe images.', required=False, + dataType='int') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getBandInformation(self, item, params): + _adjustParams(params) + params = self._parseParams(params, True, [ + ('frame', int), + ]) + _handleETag('getBandInformation', item, params) + result = self.imageItemModel.getBandInformation(item, **params) + return result
+ + +
+[docs] + @describeRoute( + Description('Get a list of additional images associated with a large image.') + .param('itemId', 'The ID of the item.', paramType='path') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def getAssociatedImagesList(self, item, params): + try: + return self.imageItemModel.getAssociatedImagesList(item) + except TileGeneralError as e: + raise RestException(e.args[0], code=400)
+ + +
+[docs] + @describeRoute( + Description('Get an image associated with a large image.') + .notes('Because associated images may contain PHI, admin access to ' + 'the item is required.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('image', 'The key of the associated image.', paramType='path') + .param('width', 'The maximum width of the image in pixels.', + required=False, dataType='int') + .param('height', 'The maximum height of the image in pixels.', + required=False, dataType='int') + .param('encoding', 'Image output encoding', required=False, + enum=['JPEG', 'PNG', 'TIFF'], default='JPEG') + .param('contentDisposition', 'Specify the Content-Disposition response ' + 'header disposition-type value.', required=False, + enum=['inline', 'attachment']) + .param('contentDispositionFilename', 'Specify the filename used in ' + 'the Content-Disposition response header.', required=False) + .produces(ImageMimeTypes) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + def getAssociatedImage(self, itemId, image, params): + _adjustParams(params) + # We can't use the loadmodel decorator, as we want to allow cookies + item = loadmodelcache.loadModel( + self, 'item', id=itemId, allowCookie=True, level=AccessType.READ) + params = self._parseParams(params, True, [ + ('width', int), + ('height', int), + ('jpegQuality', int), + ('jpegSubsampling', int), + ('tiffCompression', str), + ('encoding', str), + ('style', str), + ('contentDisposition', str), + ('contentDispositionFileName', str), + ]) + _handleETag('getAssociatedImage', item, image, params) + try: + result = self.imageItemModel.getAssociatedImage(item, image, **params) + except TileGeneralError as e: + raise RestException(e.args[0], code=400) + if not isinstance(result, tuple): + return result + imageData, imageMime = result + self._setContentDisposition( + item, params.get('contentDisposition'), imageMime, image, + params.get('contentDispositionFilename')) + setResponseHeader('Content-Type', imageMime) + setRawResponse() + return imageData
+ + +
+[docs] + @autoDescribeRoute( + Description('Get metadata for an image associated with a large image.') + .modelParam('itemId', model=Item, level=AccessType.READ) + .param('image', 'The key of the associated image.', paramType='path') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(scope=TokenScope.DATA_READ) + def getAssociatedImageMetadata(self, item, image, params): + _handleETag('getAssociatedImageMetadata', item, image) + tilesource = self.imageItemModel._loadTileSource(item, **params) + pilImage = tilesource._getAssociatedImage(image) + if pilImage is None: + return {} + result = { + 'sizeX': pilImage.width, + 'sizeY': pilImage.height, + 'mode': pilImage.mode, + } + if pilImage.format: + result['format'] = pilImage.format + if pilImage.info: + result['info'] = pilImage.info + return result
+ + + _tileFramesParams = [ + ('framesAcross', int), + ('frameList', str), + ('left', float, 'region', 'left'), + ('top', float, 'region', 'top'), + ('right', float, 'region', 'right'), + ('bottom', float, 'region', 'bottom'), + ('regionWidth', float, 'region', 'width'), + ('regionHeight', float, 'region', 'height'), + ('units', str, 'region', 'units'), + ('unitsWH', str, 'region', 'unitsWH'), + ('width', int, 'output', 'maxWidth'), + ('height', int, 'output', 'maxHeight'), + ('fill', str), + ('magnification', float, 'scale', 'magnification'), + ('mm_x', float, 'scale', 'mm_x'), + ('mm_y', float, 'scale', 'mm_y'), + ('exact', bool, 'scale', 'exact'), + ('frame', int), + ('encoding', str), + ('jpegQuality', int), + ('jpegSubsampling', int), + ('tiffCompression', str), + ('style', str), + ('resample', 'boolOrInt'), + ('contentDisposition', str), + ('contentDispositionFileName', str), + ] + +
+[docs] + @describeRoute( + Description('Composite thumbnails of multiple frames into a single image.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('framesAcross', 'How many frames across', required=False, dataType='int') + .param('frameList', 'Comma-separated list of frames', required=False) + .param('cache', 'Cache the results for future use', required=False, + dataType='boolean', default=False) + .param('left', 'The left column (0-based) of the region to process. ' + 'Negative values are offsets from the right edge.', + required=False, dataType='float') + .param('top', 'The top row (0-based) of the region to process. ' + 'Negative values are offsets from the bottom edge.', + required=False, dataType='float') + .param('right', 'The right column (0-based from the left) of the ' + 'region to process. The region will not include this column. ' + 'Negative values are offsets from the right edge.', + required=False, dataType='float') + .param('bottom', 'The bottom row (0-based from the top) of the region ' + 'to process. The region will not include this row. Negative ' + 'values are offsets from the bottom edge.', + required=False, dataType='float') + .param('regionWidth', 'The width of the region to process.', + required=False, dataType='float') + .param('regionHeight', 'The height of the region to process.', + required=False, dataType='float') + .param('units', 'Units used for left, top, right, bottom, ' + 'regionWidth, and regionHeight. base_pixels are pixels at the ' + 'maximum resolution, pixels and mm are at the specified ' + 'magnfication, fraction is a scale of [0-1].', required=False, + enum=sorted(set(TileInputUnits.values())), + default='base_pixels') + + .param('width', 'The maximum width of the output image in pixels.', + required=False, dataType='int') + .param('height', 'The maximum height of the output image in pixels.', + required=False, dataType='int') + .param('fill', 'A fill color. If output dimensions are specified and ' + 'fill is specified and not "none", the output image is padded ' + 'on either the sides or the top and bottom to the requested ' + 'output size. Most css colors are accepted.', required=False) + .param('magnification', 'Magnification of the output image. If ' + 'neither width for height is specified, the magnification, ' + 'mm_x, and mm_y parameters are used to select the output size.', + required=False, dataType='float') + .param('mm_x', 'The size of the output pixels in millimeters', + required=False, dataType='float') + .param('mm_y', 'The size of the output pixels in millimeters', + required=False, dataType='float') + .param('exact', 'If magnification, mm_x, or mm_y are specified, they ' + 'must match an existing level of the image exactly.', + required=False, dataType='boolean', default=False) + .param('frame', 'For multiframe images, the 0-based frame number. ' + 'This is ignored on non-multiframe images.', required=False, + dataType='int') + .param('encoding', 'Output image encoding. TILED generates a tiled ' + 'tiff without the upper limit on image size the other options ' + 'have. For geospatial sources, TILED will also have ' + 'appropriate tagging. Pickle emits python pickle data with an ' + 'optional specific protocol', required=False, + enum=EncodingTypes, default='JPEG') + .param('jpegQuality', 'Quality used for generating JPEG images', + required=False, dataType='int', default=95) + .param('jpegSubsampling', 'Chroma subsampling used for generating ' + 'JPEG images. 0, 1, and 2 are full, half, and quarter ' + 'resolution chroma respectively.', required=False, + enum=['0', '1', '2'], dataType='int', default='0') + .param('tiffCompression', 'Compression method when storing a TIFF ' + 'image', required=False, + enum=['none', 'raw', 'lzw', 'tiff_lzw', 'jpeg', 'deflate', + 'tiff_adobe_deflate']) + .param('style', 'JSON-encoded style string', required=False) + .param('resample', 'If false, an existing level of the image is used ' + 'for the region. If true, the internal values are ' + 'interpolated to match the specified size as needed. 0-3 for ' + 'a specific interpolation method (0-nearest, 1-lanczos, ' + '2-bilinear, 3-bicubic)', required=False, + enum=['false', 'true', '0', '1', '2', '3']) + .param('contentDisposition', 'Specify the Content-Disposition response ' + 'header disposition-type value.', required=False, + enum=['inline', 'attachment']) + .param('contentDispositionFilename', 'Specify the filename used in ' + 'the Content-Disposition response header.', required=False) + .produces(ImageMimeTypes) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403) + .errorResponse('Insufficient memory.'), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def tileFrames(self, item, params): + cache = params.pop('cache', False) + checkAndCreate = False if cache else 'nosave' + _adjustParams(params) + + params = self._parseParams(params, True, self._tileFramesParams) + _handleETag('tileFrames', item, params) + pickle = _pickleParams(params) + if 'frameList' in params: + params['frameList'] = [ + int(f.strip()) for f in str(params['frameList']).lstrip( + '[').rstrip(']').split(',')] + setResponseTimeLimit(86400) + try: + result = self.imageItemModel.tileFrames( + item, checkAndCreate=checkAndCreate, **params) + except TileGeneralError as e: + raise RestException(e.args[0]) + except ValueError as e: + raise RestException('Value Error: %s' % e.args[0]) + if not isinstance(result, tuple): + return result + regionData, regionMime = result + if pickle: + regionData, regionMime = _pickleOutput(regionData, pickle) + self._setContentDisposition( + item, params.get('contentDisposition'), regionMime, 'tileframes', + params.get('contentDispositionFilename')) + setResponseHeader('Content-Type', regionMime) + if isinstance(regionData, pathlib.Path): + BUF_SIZE = 65536 + + def stream(): + try: + with regionData.open('rb') as f: + while True: + data = f.read(BUF_SIZE) + if not data: + break + yield data + finally: + regionData.unlink() + return stream + setRawResponse() + return regionData
+ + +
+[docs] + @describeRoute( + Description('Get parameters for using tile_frames as background sprite images.') + .param('itemId', 'The ID of the item.', paramType='path') + .param('format', 'Optional format parameters, such as "encoding=JPEG&' + 'jpegQuality=85&jpegSubsampling=1". If specified, these ' + 'replace the defaults.', required=False) + .param('query', 'Addition query parameters that would be passed to ' + 'tile endpoints, such as style.', required=False) + .param('frameBase', 'Starting frame number (default 0). If c/z/t/xy ' + 'then step through values from 0 to number of that axis - 1. ' + 'The axis specification in only useful for cache reporting or ' + 'scheduling', + required=False, dataType='int') + .param('frameStride', 'Only use every frameStride frame of the image ' + '(default 1). c/z/t/xy to use the length of that axis', + required=False, dataType='int') + .param('frameGroup', 'Group frames when using multiple textures to ' + 'keep boundaries at a multiple of the group size number. ' + 'c/z/t/xy to use the length of that axis.', + required=False, dataType='int') + .param('frameGroupFactor', 'Ignore grouping if the resultant images ' + 'would be more than this factor smaller than without grouping ' + '(default 4)', required=False, dataType='int') + .param('frameGroupStride', 'Reorder frames based on the to stride ' + '(default 1). "auto" to use frameGroup / frameStride if that ' + 'value is an integer.', + required=False, dataType='int') + .param('maxTextureSize', 'Maximum texture size in either dimension. ' + 'This should be the smaller of a desired value and of the ' + 'intended graphics environment texture buffer (default 16384).', + required=False, dataType='int') + .param('maxTextures', 'Maximum number of textures to use (default 1).', + required=False, dataType='int') + .param('maxTotalTexturePixels', 'Limit the total area of all combined ' + 'textures (default 2**30).', + required=False, dataType='int') + .param('alignment', 'Individual frame alignment within a texture. ' + 'Used to avoid jpeg artifacts from crossing frames (default ' + '16).', + required=False, dataType='int') + .param('maxFrameSize', 'If specified, frames will never be larger ' + 'than this, even if the texture size allows it (default None).', + required=False, dataType='int') + .param('cache', 'Report on or request caching the resultant frames. ' + 'Scheduling creates a local job.', + required=False, + enum=['none', 'report', 'schedule']) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + @loadmodel(model='item', map={'itemId': 'item'}, level=AccessType.READ) + def tileFramesQuadInfo(self, item, params): + metadata = self.imageItemModel.getMetadata(item) + options = self._parseParams(params, False, [ + ('format', str), + ('query', str), + ('frameBase', str), + ('frameStride', str), + ('frameGroup', str), + ('frameGroupFactor', int), + ('frameGroupStride', str), + ('maxTextureSize', int), + ('maxTextures', int), + ('maxTotalTexturePixels', int), + ('alignment', int), + ('maxFrameSize', int), + ]) + for key in {'format', 'query'}: + if key in options: + options[key] = dict(urllib.parse.parse_qsl(options[key])) + result = large_image.tilesource.utilities.getTileFramesQuadInfo(metadata, options) + if params.get('cache') in {'report', 'schedule'}: + needed = [] + result['cached'] = [] + for src in result['src']: + tfParams = self._parseParams(src, False, self._tileFramesParams) + if 'frameList' in tfParams: + tfParams['frameList'] = [ + int(f.strip()) for f in str(tfParams['frameList']).lstrip( + '[').rstrip(']').split(',')] + result['cached'].append(self.imageItemModel.tileFrames( + item, checkAndCreate='check', **tfParams)) + if not result['cached'][-1]: + needed.append(tfParams) + if params.get('cache') == 'schedule' and not all(result['cached']): + result['scheduledJob'] = str(self.imageItemModel._scheduleTileFrames( + item, needed, self.getCurrentUser())['_id']) + return result
+ + +
+[docs] + @autoDescribeRoute( + Description('List all thumbnail and data files associated with a large_image item.') + .modelParam('itemId', model=Item, level=AccessType.READ) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.admin(scope=TokenScope.DATA_READ) + def listTilesThumbnails(self, item): + return self.imageItemModel.removeThumbnailFiles(item, onlyList=True)
+ + +
+[docs] + @autoDescribeRoute( + Description('Delete thumbnail and data files associated with a large_image item.') + .modelParam('itemId', model=Item, level=AccessType.READ) + .param('keep', 'Number of thumbnails to keep. Ignored if a key is ' + 'specified.', dataType='integer', required=False, + default=10000) + .param('key', 'A specific key to delete', required=False) + .param('thumbnail', 'If a key is specified, true if the key is a ' + 'thumbnail; false if the key is a data record', + dataType='boolean', required=False) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def deleteTilesThumbnails(self, item, keep, key=None, thumbnail=True): + if not key: + return self.imageItemModel.removeThumbnailFiles(item, keep=keep or 0) + thumbnail = str(thumbnail).lower() != 'false' + query = { + 'attachedToType': 'item', + 'attachedToId': item['_id'], + 'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True, + 'thumbnailKey': key, + } + file = File().findOne(query) + if file: + File().remove(file) + return [file]
+ + +
+[docs] + @autoDescribeRoute( + Description('Associate or replace a thumbnail or data file with a large_image items.') + .responseClass('File') + .modelParam('itemId', model=Item, level=AccessType.WRITE) + .param('key', 'A specific key to delete', required=True) + .param('thumbnail', 'If a key is specified, true if the key is a ' + 'thumbnail; false if the key is a data record', + dataType='boolean', required=False) + .param('mimeType', 'The MIME type of the file.', required=False) + .param('data', 'An image or data block to associated with the large_image item.', + paramType='body', dataType='binary') + .consumes('application/octet-stream') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.user(scope=TokenScope.DATA_WRITE) + def addTilesThumbnails(self, item, key, mimeType, thumbnail=False, data=None): + user = self.getCurrentUser() + thumbnail = str(thumbnail).lower() != 'false' + query = { + 'attachedToType': 'item', + 'attachedToId': item['_id'], + 'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True, + 'thumbnailKey': key, + } + file = File().findOne(query) + if file: + File().remove(file) + data = cherrypy.request.body.read() + try: + import magic + mimeType = magic.from_buffer(data, mime=True) or mimeType + except Exception: + pass + mimeType = mimeType or 'application/octet-stream' + datafile = Upload().uploadFromFile( + io.BytesIO(data), size=len(data), + name='_largeImageThumbnail', parentType='item', parent=item, + user=user, mimeType=mimeType, attachParent=True) + datafile.update({ + 'isLargeImageThumbnail' if thumbnail is not False else 'isLargeImageData': True, + 'thumbnailKey': key, + }) + datafile = File().save(datafile) + return datafile
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image_annotation.html b/_modules/girder_large_image_annotation.html new file mode 100644 index 000000000..34a135932 --- /dev/null +++ b/_modules/girder_large_image_annotation.html @@ -0,0 +1,235 @@ + + + + + + girder_large_image_annotation — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for girder_large_image_annotation

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+from girder import events
+from girder.constants import registerAccessFlag
+from girder.exceptions import ValidationException
+from girder.plugin import GirderPlugin, getPlugin
+from girder.settings import SettingDefault
+from girder.utility import search, setting_utilities
+from girder.utility.model_importer import ModelImporter
+
+from . import constants, handlers
+from .models.annotation import Annotation
+from .rest.annotation import AnnotationResource
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+
+[docs] +def metadataSearchHandler(*args, **kwargs): + import girder_large_image + + return girder_large_image.metadataSearchHandler( + *args, + models=['item'], + searchModels={('annotation', 'large_image'): {'model': 'item', 'reference': 'itemId'}}, + metakey='annotation.attributes', **kwargs)
+ + + +# Validators + +
+[docs] +@setting_utilities.validator({ + constants.PluginSettings.LARGE_IMAGE_ANNOTATION_HISTORY, +}) +def validateBoolean(doc): + val = doc['value'] + if str(val).lower() not in ('false', 'true', ''): + raise ValidationException('%s must be a boolean.' % doc['key'], 'value') + doc['value'] = (str(val).lower() != 'false')
+ + + +# Defaults + +# Defaults that have fixed values can just be added to the system defaults +# dictionary. +SettingDefault.defaults.update({ + constants.PluginSettings.LARGE_IMAGE_ANNOTATION_HISTORY: True, +}) + +# Access flags + +registerAccessFlag(constants.ANNOTATION_ACCESS_FLAG, 'Create annotations', + 'Allow user to create annotations') + + +
+[docs] +class LargeImageAnnotationPlugin(GirderPlugin): + DISPLAY_NAME = 'Large Image Annotation' + CLIENT_SOURCE_PATH = 'web_client' + +
+[docs] + def load(self, info): + getPlugin('large_image').load(info) + + ModelImporter.registerModel('annotation', Annotation, 'large_image') + info['apiRoot'].annotation = AnnotationResource() + # Ask for some models to make sure their singletons are initialized. + # Also migrate the database as a one-time action. + Annotation()._migrateDatabase() + + # add copyAnnotations option to POST resource/copy, POST item/{id}/copy + # and POST folder/{id}/copy + info['apiRoot'].resource.copyResources.description.param( + 'copyAnnotations', 'Copy annotations when copying resources (default true)', + required=False, dataType='boolean') + info['apiRoot'].item.copyItem.description.param( + 'copyAnnotations', 'Copy annotations when copying item (default true)', + required=False, dataType='boolean') + info['apiRoot'].folder.copyFolder.description.param( + 'copyAnnotations', 'Copy annotations when copying folder (default true)', + required=False, dataType='boolean') + + events.bind( + 'data.process', 'large_image_annotation.annotations', + handlers.process_annotations) + + search._allowedSearchMode.pop('li_annotation_metadata', None) + search.addSearchMode('li_annotation_metadata', metadataSearchHandler)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image_annotation/handlers.html b/_modules/girder_large_image_annotation/handlers.html new file mode 100644 index 000000000..877dc14cd --- /dev/null +++ b/_modules/girder_large_image_annotation/handlers.html @@ -0,0 +1,291 @@ + + + + + + girder_large_image_annotation.handlers — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image_annotation.handlers

+import json
+import time
+import uuid
+
+import cachetools
+import orjson
+
+import large_image.config
+from girder import logger
+from girder.constants import AccessType
+from girder.models.file import File
+from girder.models.item import Item
+from girder.models.user import User
+
+from .models.annotation import Annotation
+from .utils import isGeoJSON
+
+_recentIdentifiers = cachetools.TTLCache(maxsize=100, ttl=86400)
+
+
+def _itemFromEvent(event, identifierEnding, itemAccessLevel=AccessType.READ):
+    """
+    If an event has a reference and an associated identifier that ends with a
+    specific string, return the associated item, user, and image file.
+
+    :param event: the data.process event.
+    :param identifierEnding: the required end of the identifier.
+    :returns: a dictionary with item, user, and file if there was a match.
+    """
+    info = event.info
+    identifier = None
+    reference = info.get('reference', None)
+    if reference is not None:
+        try:
+            reference = json.loads(reference)
+            if (isinstance(reference, dict) and
+                    isinstance(reference.get('identifier'), str)):
+                identifier = reference['identifier']
+        except (ValueError, TypeError):
+            logger.debug('Failed to parse data.process reference: %r', reference)
+    if identifier and 'uuid' in reference:
+        if reference['uuid'] not in _recentIdentifiers:
+            _recentIdentifiers[reference['uuid']] = {}
+        _recentIdentifiers[reference['uuid']][identifier] = info
+        reprocessFunc = _recentIdentifiers[reference['uuid']].pop('_reprocess', None)
+        if reprocessFunc:
+            reprocessFunc()
+    if identifier is not None and identifier.endswith(identifierEnding):
+        if identifier == 'LargeImageAnnotationUpload' and 'uuid' not in reference:
+            reference['uuid'] = str(uuid.uuid4())
+        if 'userId' not in reference or 'itemId' not in reference or 'fileId' not in reference:
+            logger.error('Reference does not contain required information.')
+            return
+
+        userId = reference['userId']
+        imageId = reference['fileId']
+
+        # load models from the database
+        user = User().load(userId, force=True)
+        image = File().load(imageId, level=AccessType.READ, user=user)
+        item = Item().load(image['itemId'], level=itemAccessLevel, user=user)
+        return {'item': item, 'user': user, 'file': image, 'uuid': reference.get('uuid')}
+
+
+
+[docs] +def resolveAnnotationGirderIds(event, results, data, possibleGirderIds): + """ + If an annotation has references to girderIds, resolve them to actual ids. + + :param event: a data.process event. + :param results: the results from _itemFromEvent, + :param data: annotation data. + :param possibleGirderIds: a list of annotation elements with girderIds + needing resolution. + :returns: True if all ids were processed. + """ + # Exclude actual girderIds from resolution + girderIds = [] + for element in possibleGirderIds: + # This will throw an exception if the girderId isn't well-formed as an + # actual id. + try: + if Item().load(element['girderId'], level=AccessType.READ, force=True) is None: + girderIds.append(element) + except Exception: + girderIds.append(element) + if not len(girderIds): + return True + idRecord = _recentIdentifiers.get(results.get('uuid')) + if idRecord and not all(element['girderId'] in idRecord for element in girderIds): + idRecord['_reprocess'] = lambda: process_annotations(event) + return False + for element in girderIds: + element['girderId'] = str(idRecord[element['girderId']]['file']['itemId']) + # Currently, all girderIds inside annotations are expected to be + # large images. In this case, load them and ask if they can be so, + # in case they are small images + from girder_large_image.models.image_item import ImageItem + + try: + item = ImageItem().load(element['girderId'], force=True) + ImageItem().createImageItem( + item, list(ImageItem().childFiles(item=item, limit=1))[0], createJob=False) + except Exception: + pass + return True
+ + + +
+[docs] +def process_annotations(event): # noqa: C901 + """Add annotations to an image on a ``data.process`` event""" + results = _itemFromEvent(event, 'LargeImageAnnotationUpload') + if not results: + return + item = results['item'] + user = results['user'] + + file = File().load( + event.info.get('file', {}).get('_id'), + level=AccessType.READ, user=user, + ) + startTime = time.time() + + if not file: + logger.error('Could not load models from the database') + return + try: + if file['size'] > int(large_image.config.getConfig( + 'max_annotation_input_file_length', 1024 ** 3)): + msg = ('File is larger than will be read into memory. If your ' + 'server will permit it, increase the ' + 'max_annotation_input_file_length setting.') + raise Exception(msg) + data = [] + with File().open(file) as fptr: + while True: + chunk = fptr.read(1024 ** 2) + if not len(chunk): + break + data.append(chunk) + data = orjson.loads(b''.join(data).decode()) + except Exception: + logger.error('Could not parse annotation file') + raise + if time.time() - startTime > 10: + logger.info('Decoded json in %5.3fs', time.time() - startTime) + + if not isinstance(data, list) or isGeoJSON(data): + data = [data] + data = [entry['annotation'] if 'annotation' in entry else entry for entry in data] + # Check some of the early elements to see if there are any girderIds + # that need resolution. + if 'uuid' in results: + girderIds = [ + element for annotation in data + for element in annotation.get('elements', [])[:100] + if 'girderId' in element] + if len(girderIds): + if not resolveAnnotationGirderIds(event, results, data, girderIds): + return + for annotation in data: + try: + Annotation().createAnnotation(item, user, annotation) + except Exception: + logger.error('Could not create annotation object from data') + raise + if str(file['itemId']) == str(item['_id']): + File().remove(file)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image_annotation/models/annotation.html b/_modules/girder_large_image_annotation/models/annotation.html new file mode 100644 index 000000000..21630a2c5 --- /dev/null +++ b/_modules/girder_large_image_annotation/models/annotation.html @@ -0,0 +1,1692 @@ + + + + + + girder_large_image_annotation.models.annotation — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image_annotation.models.annotation

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import copy
+import datetime
+import enum
+import re
+import threading
+import time
+
+import cherrypy
+import jsonschema
+import numpy as np
+from bson import ObjectId
+from girder_large_image import constants
+from girder_large_image.models.image_item import ImageItem
+
+from girder import events, logger
+from girder.constants import AccessType, SortDir
+from girder.exceptions import AccessException, ValidationException
+from girder.models.folder import Folder
+from girder.models.item import Item
+from girder.models.model_base import AccessControlledModel
+from girder.models.notification import Notification
+from girder.models.setting import Setting
+from girder.models.user import User
+
+from ..utils import AnnotationGeoJSON, GeoJSONAnnotation, isGeoJSON
+from .annotationelement import Annotationelement
+
+# Some arrays longer than this are validated using numpy rather than jsonschema
+VALIDATE_ARRAY_LENGTH = 1000
+
+
+
+[docs] +def extendSchema(base, add): + extend = copy.deepcopy(base) + for key in add: + if key == 'required' and 'required' in base: + extend[key] = sorted(set(extend[key]) | set(add[key])) + elif key != 'properties' and 'properties' in base: + extend[key] = add[key] + if 'properties' in add: + extend['properties'].update(add['properties']) + return extend
+ + + +
+[docs] +class AnnotationSchema: + coordSchema = { + 'type': 'array', + # TODO: validate that z==0 for now + 'items': { + 'type': 'number', + }, + 'minItems': 3, + 'maxItems': 3, + 'name': 'Coordinate', + # TODO: define origin for 3D images + 'description': 'An X, Y, Z coordinate tuple, in base layer pixel ' + 'coordinates, where the origin is the upper-left.', + } + coordValueSchema = { + 'type': 'array', + 'items': { + 'type': 'number', + }, + 'minItems': 4, + 'maxItems': 4, + 'name': 'CoordinateWithValue', + 'description': 'An X, Y, Z, value coordinate tuple, in base layer ' + 'pixel coordinates, where the origin is the upper-left.', + } + + colorSchema = { + 'type': 'string', + # We accept colors of the form + # #rrggbb six digit RRGGBB hex + # #rgb three digit RGB hex + # #rrggbbaa eight digit RRGGBBAA hex + # #rgba four digit RGBA hex + # rgb(255, 255, 255) rgb decimal triplet + # rgba(255, 255, 255, 1) rgba quad with RGB in the range [0-255] and + # alpha [0-1] + # TODO: make rgb and rgba spec validate that rgb is [0-255] and a is + # [0-1], rather than just checking if they are digits and such. + 'pattern': r'^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|' + r'rgb\(\d+,\s*\d+,\s*\d+\)|' + r'rgba\(\d+,\s*\d+,\s*\d+,\s*(\d?\.|)\d+\))$', + } + + colorRangeSchema = { + 'type': 'array', + 'items': colorSchema, + 'description': 'A list of colors', + } + + rangeValueSchema = { + 'type': 'array', + 'items': {'type': 'number'}, + 'description': 'A weakly monotonic list of range values', + } + + userSchema = { + 'type': 'object', + 'additionalProperties': True, + } + + labelSchema = { + 'type': 'object', + 'properties': { + 'value': {'type': 'string'}, + 'visibility': { + 'type': 'string', + # TODO: change to True, False, None? + 'enum': ['hidden', 'always', 'onhover'], + }, + 'fontSize': { + 'type': 'number', + 'exclusiveMinimum': 0, + }, + 'color': colorSchema, + }, + 'required': ['value'], + 'additionalProperties': False, + } + + groupSchema = {'type': 'string'} + + baseElementSchema = { + 'type': 'object', + 'properties': { + 'id': { + 'type': 'string', + 'pattern': '^[0-9a-f]{24}$', + }, + 'type': {'type': 'string'}, + # schema free field for users to extend annotations + 'user': userSchema, + 'label': labelSchema, + 'group': groupSchema, + }, + 'required': ['type'], + 'additionalProperties': True, + } + baseShapeSchema = extendSchema(baseElementSchema, { + 'properties': { + 'lineColor': colorSchema, + 'lineWidth': { + 'type': 'number', + 'minimum': 0, + }, + }, + }) + + pointShapeSchema = extendSchema(baseShapeSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['point'], + }, + 'center': coordSchema, + 'fillColor': colorSchema, + }, + 'required': ['type', 'center'], + 'additionalProperties': False, + }) + + arrowShapeSchema = extendSchema(baseShapeSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['arrow'], + }, + 'points': { + 'type': 'array', + 'items': coordSchema, + 'minItems': 2, + 'maxItems': 2, + }, + 'fillColor': colorSchema, + }, + 'description': 'The first point is the head of the arrow', + 'required': ['type', 'points'], + 'additionalProperties': False, + }) + + circleShapeSchema = extendSchema(baseShapeSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['circle'], + }, + 'center': coordSchema, + 'radius': { + 'type': 'number', + 'minimum': 0, + }, + 'fillColor': colorSchema, + }, + 'required': ['type', 'center', 'radius'], + 'additionalProperties': False, + }) + + polylineShapeSchema = extendSchema(baseShapeSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['polyline'], + }, + 'points': { + 'type': 'array', + 'items': coordSchema, + 'minItems': 2, + }, + 'fillColor': colorSchema, + 'closed': { + 'type': 'boolean', + 'description': 'polyline is open if closed flag is ' + 'not specified', + }, + 'holes': { + 'type': 'array', + 'description': + 'If closed is true, this is a list of polylines that are ' + 'treated as holes in the base polygon. These should not ' + 'cross each other and should be contained within the base ' + 'polygon.', + 'items': { + 'type': 'array', + 'items': coordSchema, + 'minItems': 3, + }, + }, + }, + 'required': ['type', 'points'], + 'additionalProperties': False, + }) + + baseRectangleShapeSchema = extendSchema(baseShapeSchema, { + 'properties': { + 'type': {'type': 'string'}, + 'center': coordSchema, + 'width': { + 'type': 'number', + 'minimum': 0, + }, + 'height': { + 'type': 'number', + 'minimum': 0, + }, + 'rotation': { + 'type': 'number', + 'description': 'radians counterclockwise around normal', + }, + 'normal': coordSchema, + 'fillColor': colorSchema, + }, + 'decription': 'normal is the positive z-axis unless otherwise ' + 'specified', + 'required': ['type', 'center', 'width', 'height'], + }) + + rectangleShapeSchema = extendSchema(baseRectangleShapeSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['rectangle'], + }, + }, + 'additionalProperties': False, + }) + rectangleGridShapeSchema = extendSchema(baseRectangleShapeSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['rectanglegrid'], + }, + 'widthSubdivisions': { + 'type': 'integer', + 'minimum': 1, + }, + 'heightSubdivisions': { + 'type': 'integer', + 'minimum': 1, + }, + }, + 'required': ['type', 'widthSubdivisions', 'heightSubdivisions'], + 'additionalProperties': False, + }) + ellipseShapeSchema = extendSchema(baseRectangleShapeSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['ellipse'], + }, + }, + 'required': ['type'], + 'additionalProperties': False, + }) + + heatmapSchema = extendSchema(baseElementSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['heatmap'], + }, + 'points': { + 'type': 'array', + 'items': coordValueSchema, + }, + 'radius': { + 'type': 'number', + 'exclusiveMinimum': 0, + }, + 'colorRange': colorRangeSchema, + 'rangeValues': rangeValueSchema, + 'normalizeRange': { + 'type': 'boolean', + 'description': + 'If true, rangeValues are on a scale of 0 to 1 ' + 'and map to the minimum and maximum values on the ' + 'data. If false (the default), the rangeValues ' + 'are the actual data values.', + }, + 'scaleWithZoom': { + 'type': 'boolean', + 'description': + 'If true, scale the size of points with the ' + 'zoom level of the map.', + }, + }, + 'required': ['type', 'points'], + 'additionalProperties': False, + 'description': + 'ColorRange and rangeValues should have a one-to-one ' + 'correspondence.', + }) + + griddataSchema = extendSchema(baseElementSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['griddata'], + }, + 'origin': coordSchema, + 'dx': { + 'type': 'number', + 'description': 'grid spacing in the x direction', + }, + 'dy': { + 'type': 'number', + 'description': 'grid spacing in the y direction', + }, + 'gridWidth': { + 'type': 'integer', + 'minimum': 1, + 'description': 'The number of values across the width of the grid', + }, + 'values': { + 'type': 'array', + 'items': {'type': 'number'}, + 'description': + 'The values of the grid. This must have a ' + 'multiple of gridWidth entries', + }, + 'interpretation': { + 'type': 'string', + 'enum': ['heatmap', 'contour', 'choropleth'], + }, + 'radius': { + 'type': 'number', + 'exclusiveMinimum': 0, + 'description': 'radius used for heatmap interpretation', + }, + 'colorRange': colorRangeSchema, + 'rangeValues': rangeValueSchema, + 'normalizeRange': { + 'type': 'boolean', + 'description': + 'If true, rangeValues are on a scale of 0 to 1 ' + 'and map to the minimum and maximum values on the ' + 'data. If false (the default), the rangeValues ' + 'are the actual data values.', + }, + 'stepped': {'type': 'boolean'}, + 'minColor': colorSchema, + 'maxColor': colorSchema, + }, + 'required': ['type', 'values', 'gridWidth'], + 'additionalProperties': False, + 'description': + 'ColorRange and rangeValues should have a one-to-one ' + 'correspondence except for stepped contours where ' + 'rangeValues needs one more entry than colorRange. ' + 'minColor and maxColor are the colors applies to values ' + 'beyond the ranges in rangeValues.', + }) + + transformArray = { + 'type': 'array', + 'items': { + 'type': 'array', + 'minItems': 2, + 'maxItems': 2, + }, + 'minItems': 2, + 'maxItems': 2, + 'description': 'A 2D matrix representing the transform of an ' + 'image overlay.', + } + + overlaySchema = extendSchema(baseElementSchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['image'], + }, + 'girderId': { + 'type': 'string', + 'pattern': '^[0-9a-f]{24}$', + 'description': 'Girder item ID containing the image to ' + 'overlay.', + }, + 'opacity': { + 'type': 'number', + 'minimum': 0, + 'maximum': 1, + 'description': 'Default opacity for this image overlay. Must ' + 'be between 0 and 1. Defaults to 1.', + }, + 'hasAlpha': { + 'type': 'boolean', + 'description': + 'If true, the image is treated assuming it has an alpha ' + 'channel.', + }, + 'transform': { + 'type': 'object', + 'description': 'Specification for an affine transform of the ' + 'image overlay. Includes a 2D transform matrix, ' + 'an X offset and a Y offset.', + 'properties': { + 'xoffset': { + 'type': 'number', + }, + 'yoffset': { + 'type': 'number', + }, + 'matrix': transformArray, + }, + }, + }, + 'required': ['girderId', 'type'], + 'additionalProperties': False, + 'description': 'An image overlay on top of the base resource.', + }) + + pixelmapCategorySchema = { + 'type': 'object', + 'properties': { + 'fillColor': colorSchema, + 'strokeColor': colorSchema, + 'label': { + 'type': 'string', + 'description': 'A string representing the semantic ' + 'meaning of regions of the map with ' + 'the corresponding color.', + }, + 'description': { + 'type': 'string', + 'description': 'A more detailed explanation of the ' + 'meaining of this category.', + }, + }, + 'required': ['fillColor'], + 'additionalProperties': False, + } + + pixelmapSchema = extendSchema(overlaySchema, { + 'properties': { + 'type': { + 'type': 'string', + 'enum': ['pixelmap'], + }, + 'values': { + 'type': 'array', + 'items': {'type': 'integer'}, + 'description': 'An array where the indices ' + 'correspond to pixel values in the ' + 'pixel map image and the values are ' + 'used to look up the appropriate ' + 'color in the categories property.', + }, + 'categories': { + 'type': 'array', + 'items': pixelmapCategorySchema, + 'description': 'An array used to map between the ' + 'values array and color values. ' + 'Can also contain semantic ' + 'information for color values.', + }, + 'boundaries': { + 'type': 'boolean', + 'description': 'True if the pixelmap doubles pixel ' + 'values such that even values are the ' + 'fill and odd values the are stroke ' + 'of each superpixel. If true, the ' + 'length of the values array should be ' + 'half of the maximum value in the ' + 'pixelmap.', + + }, + }, + 'required': ['values', 'categories', 'boundaries'], + 'additionalProperties': False, + 'description': 'A tiled pixelmap to overlay onto a base resource.', + }) + + annotationElementSchema = { + # Shape subtypes are mutually exclusive, so for efficiency, don't use + # 'oneOf' + 'anyOf': [ + # If we include the baseShapeSchema, then shapes that are as-yet + # invented can be included. + # baseShapeSchema, + arrowShapeSchema, + circleShapeSchema, + ellipseShapeSchema, + griddataSchema, + heatmapSchema, + pointShapeSchema, + polylineShapeSchema, + rectangleShapeSchema, + rectangleGridShapeSchema, + overlaySchema, + pixelmapSchema, + ], + } + + annotationSchema = { + '$schema': 'http://json-schema.org/schema#', + 'type': 'object', + 'properties': { + 'name': { + 'type': 'string', + # TODO: Disallow empty? + 'minLength': 1, + }, + 'description': {'type': 'string'}, + 'display': { + 'type': 'object', + 'properties': { + 'visible': { + 'type': ['boolean', 'string'], + 'enum': ['new', True, False], + 'description': 'This advises viewers on when the ' + 'annotation should be shown. If "new" (the default), ' + 'show the annotation when it is first added to the ' + "system. If false, don't show the annotation by " + 'default. If true, show the annotation when the item ' + 'is displayed.', + }, + }, + }, + 'attributes': { + 'type': 'object', + 'additionalProperties': True, + 'title': 'Image Attributes', + 'description': 'Subjective things that apply to the entire ' + 'image.', + }, + 'elements': { + 'type': 'array', + 'items': annotationElementSchema, + # We want to ensure unique element IDs, if they are set. If + # they are not set, we assign them from Mongo. + 'title': 'Image Markup', + 'description': 'Subjective things that apply to a ' + 'spatial region.', + }, + }, + 'additionalProperties': False, + }
+ + + +
+[docs] +class Annotation(AccessControlledModel): + """ + This model is used to represent an annotation that is associated with an + item. The annotation can contain any number of annotationelements, which + are included because they reference this annotation as a parent. The + annotation acts like these are a native part of it, though they are each + stored as independent models to (eventually) permit faster spatial + searching. + """ + + validatorAnnotation = jsonschema.Draft6Validator( + AnnotationSchema.annotationSchema) + validatorAnnotationElement = jsonschema.Draft6Validator( + AnnotationSchema.annotationElementSchema) + idRegex = re.compile('^[0-9a-f]{24}$') + numberInstance = (int, float) + +
+[docs] + class Skill(enum.Enum): + NOVICE = 'novice' + EXPERT = 'expert'
+ + + # This is everything except the annotation field, and is used, in part, to + # determine what gets returned in a general find. + baseFields = ( + '_id', + 'itemId', + 'creatorId', + 'created', + 'updated', + 'updatedId', + 'public', + 'publicFlags', + 'groups', + # 'skill', + # 'startTime' + # 'stopTime' + ) + +
+[docs] + def initialize(self): + self._writeLock = threading.Lock() + self.name = 'annotation' + self.ensureIndices([ + 'itemId', + 'created', + 'creatorId', + ([ + ('itemId', SortDir.ASCENDING), + ('_active', SortDir.ASCENDING), + ], {}), + ([ + ('_id', SortDir.ASCENDING), + ('_version', SortDir.DESCENDING), + ], {}), + '_version', + 'updated', + ]) + self.ensureTextIndex({ + 'annotation.name': 10, + 'annotation.description': 1, + }) + + self.exposeFields(AccessType.READ, ( + 'annotation', '_version', '_elementQuery', '_active', + ) + self.baseFields) + events.bind('model.item.remove', 'large_image_annotation', self._onItemRemove) + events.bind('model.item.copy.prepare', 'large_image_annotation', self._prepareCopyItem) + events.bind('model.item.copy.after', 'large_image_annotation', self._handleCopyItem) + + self._historyEnabled = Setting().get( + constants.PluginSettings.LARGE_IMAGE_ANNOTATION_HISTORY) + # Listen for changes to our relevant settings + events.bind('model.setting.save.after', 'large_image_annotation', self._onSettingChange) + events.bind('model.setting.remove', 'large_image_annotation', self._onSettingChange)
+ + + def _onItemRemove(self, event): + """ + When an item is removed, also delete associated annotations. + + :param event: the event with the item information. + """ + item = event.info + annotations = Annotation().find({'itemId': item['_id']}) + for annotation in annotations: + if self._historyEnabled: + # just mark the annotations as inactive + self.update({'_id': annotation['_id']}, {'$set': {'_active': False}}) + else: + Annotation().remove(annotation) + + def _prepareCopyItem(self, event): + # check if this copy should include annotations + if (cherrypy.request and cherrypy.request.params and + str(cherrypy.request.params.get('copyAnnotations')).lower() == 'false'): + return + srcItem, newItem = event.info + if Annotation().findOne({ + '_active': {'$ne': False}, 'itemId': srcItem['_id']}): + newItem['_annotationItemId'] = srcItem['_id'] + Item().save(newItem, triggerEvents=False) + + def _handleCopyItem(self, event): + newItem = event.info + srcItemId = newItem.pop('_annotationItemId', None) + if srcItemId: + Item().save(newItem, triggerEvents=False) + self._copyAnnotationsFromOtherItem(srcItemId, newItem) + + def _copyAnnotationsFromOtherItem(self, srcItemId, destItem): + # Copy annotations from the original item to this one + query = {'_active': {'$ne': False}, 'itemId': srcItemId} + annotations = Annotation().find(query) + total = annotations.count() + if not total: + return + destItemId = destItem['_id'] + folder = Folder().load(destItem['folderId'], force=True) + count = 0 + for annotation in annotations: + logger.info('Copying annotation %d of %d from %s to %s', + count + 1, total, srcItemId, destItemId) + # Make sure we have the elements + annotation = Annotation().load(annotation['_id'], force=True) + # This could happen, for instance, if the annotation were deleted + # while we are copying other annotations. + if annotation is None: + continue + annotation['itemId'] = destItemId + del annotation['_id'] + # Remove existing permissions, then give it the same permissions + # as the item's folder. + annotation.pop('access', None) + self.copyAccessPolicies(destItem, annotation, save=False) + self.setPublic(annotation, folder.get('public'), save=False) + self.save(annotation) + count += 1 + logger.info('Copied %d annotations from %s to %s ', + count, srcItemId, destItemId) + + def _onSettingChange(self, event): + settingDoc = event.info + if settingDoc['key'] == constants.PluginSettings.LARGE_IMAGE_ANNOTATION_HISTORY: + self._historyEnabled = settingDoc['value'] + + def _migrateDatabase(self): + # Check that all entries have ACL + for annotation in self.collection.find({'access': {'$exists': False}}): + self._migrateACL(annotation) + # Check that all annotations have groups + for annotation in self.collection.find({'groups': {'$exists': False}}): + self.injectAnnotationGroupSet(annotation) + + def _migrateACL(self, annotation): + """ + Add access control information to an annotation model. + + Originally annotation models were not access controlled. This function + performs the migration for annotations created before this change was + made. The access object is copied from the folder containing the image + the annotation is attached to. In addition, the creator is given + admin access. + """ + if annotation is None or 'access' in annotation: + return annotation + + item = Item().load(annotation['itemId'], force=True) + if item is None: + logger.debug( + 'Could not generate annotation ACL due to missing item %s', annotation['_id']) + return annotation + + folder = Folder().load(item['folderId'], force=True) + if folder is None: + logger.debug( + 'Could not generate annotation ACL due to missing folder %s', annotation['_id']) + return annotation + + user = None + if annotation.get('creatorId'): + user = User().load(annotation['creatorId'], force=True) + if user is None: + logger.debug( + 'Could not generate annotation ACL due to missing user %s', annotation['_id']) + return annotation + + self.copyAccessPolicies(item, annotation, save=False) + self.setUserAccess(annotation, user, AccessType.ADMIN, force=True, save=False) + self.setPublic(annotation, folder.get('public') or False, save=False) + + # call the super class save method to avoid messing with elements + super().save(annotation) + logger.info('Generated annotation ACL for %s', annotation['_id']) + return annotation + +
+[docs] + def createAnnotation(self, item, creator, annotation, public=None): + if isGeoJSON(annotation): + geojson = GeoJSONAnnotation(annotation) + if geojson.elementCount: + annotation = geojson.annotation + now = datetime.datetime.now(datetime.timezone.utc) + doc = { + 'itemId': item['_id'], + 'creatorId': creator['_id'], + 'created': now, + 'updatedId': creator['_id'], + 'updated': now, + 'annotation': annotation, + } + if annotation and not annotation.get('name'): + annotation['name'] = now.strftime('Annotation %Y-%m-%d %H:%M') + + # copy access control from the folder containing the image + folder = Folder().load(item['folderId'], force=True) + self.copyAccessPolicies(src=folder, dest=doc, save=False) + + if public is None: + public = folder.get('public', False) + self.setPublic(doc, public, save=False) + + # give the current user admin access + self.setUserAccess(doc, user=creator, level=AccessType.ADMIN, save=False) + + doc = self.save(doc) + Notification().createNotification( + type='large_image_annotation.create', + data={'_id': doc['_id'], 'itemId': doc['itemId']}, + user=creator, + expires=datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1)) + return doc
+ + +
+[docs] + def load(self, id, region=None, getElements=True, *args, **kwargs): + """ + Load an annotation, adding all or a subset of the elements to it. + + :param region: if present, a dictionary restricting which annotations + are returned. See annotationelement.getElements. + :param getElements: if False, don't get elements associated with this + annotation. + :returns: the matching annotation or none. + """ + annotation = super().load(id, *args, **kwargs) + if annotation is None: + return + + if getElements: + # It is possible that we are trying to read the elements of an + # annotation as another thread is updating them. In this case, + # there is a chance, that between when we get the annotation and + # ask for the elements, the version will have been updated and the + # elements will have gone away. To work around the lack of + # transactions in Mongo, if we don't get any elements, we check if + # the version has shifted under us, and, if so, requery. I've put + # an arbitrary retry limit on this to prevent an infinite loop. + maxRetries = 3 + for retry in range(maxRetries): + Annotationelement().getElements( + annotation, region) + if (len(annotation.get('annotation', {}).get('elements')) or + retry + 1 == maxRetries): + break + recheck = super().load(id, *args, **kwargs) + if (recheck is None or + annotation.get('_version') == recheck.get('_version')): + break + annotation = recheck + + self.injectAnnotationGroupSet(annotation) + return annotation
+ + +
+[docs] + def remove(self, annotation, *args, **kwargs): + """ + When removing an annotation, remove all element associated with it. + This overrides the collection delete_one method so that all of the + triggers are fired as expected and cancelling from an event will work + as needed. + + :param annotation: the annotation document to remove. + """ + if self._historyEnabled: + # just mark the annotations as inactive + result = self.update({'_id': annotation['_id']}, {'$set': {'_active': False}}) + else: + with self._writeLock: + delete_one = self.collection.delete_one + + def deleteElements(query, *args, **kwargs): + ret = delete_one(query, *args, **kwargs) + Annotationelement().removeElements(annotation) + return ret + + with self._writeLock: + self.collection.delete_one = deleteElements + try: + result = super().remove(annotation, *args, **kwargs) + finally: + self.collection.delete_one = delete_one + Notification().createNotification( + type='large_image_annotation.remove', + data={'_id': annotation['_id'], 'itemId': annotation['itemId']}, + user=User().load(annotation['creatorId'], force=True), + expires=datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1)) + return result
+ + +
+[docs] + def save(self, annotation, *args, **kwargs): + """ + When saving an annotation, override the collection insert_one and + replace_one methods so that we don't save the elements with the main + annotation. Still use the super class's save method, so that all of + the triggers are fired as expected and cancelling and modifications can + be done as needed. + + Because Mongo doesn't support transactions, a version number is stored + with the annotation and with the associated elements. This is used to + add the new elements first, then update the annotation, and delete the + old elements. The allows version integrity if another thread queries + the annotation at the same time. + + :param annotation: the annotation document to save. + :returns: the saved document. If it is a new document, the _id has + been added. + """ + starttime = time.time() + with self._writeLock: + replace_one = self.collection.replace_one + insert_one = self.collection.insert_one + version = Annotationelement().getNextVersionValue() + if '_id' not in annotation: + oldversion = None + else: + if '_annotationId' in annotation: + annotation['_id'] = annotation['_annotationId'] + # We read the old version from the existing record, because we + # don't want to trust that the input _version has not been altered + # or is present. + oldversion = self.collection.find_one( + {'_id': annotation['_id']}).get('_version') + annotation['_version'] = version + _elementQuery = annotation.pop('_elementQuery', None) + annotation.pop('_active', None) + annotation.pop('_annotationId', None) + + def replaceElements(query, doc, *args, **kwargs): + Annotationelement().updateElements(doc) + elements = doc['annotation'].pop('elements', None) + if self._historyEnabled: + oldAnnotation = self.collection.find_one(query) + if oldAnnotation: + oldAnnotation['_annotationId'] = oldAnnotation.pop('_id') + oldAnnotation['_active'] = False + insert_one(oldAnnotation) + ret = replace_one(query, doc, *args, **kwargs) + if elements: + doc['annotation']['elements'] = elements + if not self._historyEnabled: + Annotationelement().removeOldElements(doc, oldversion) + return ret + + def insertElements(doc, *args, **kwargs): + # When creating an annotation, store the elements first, then store + # the annotation without elements, then restore the elements. + doc.setdefault('_id', ObjectId()) + if doc['annotation'].get('elements') is not None: + Annotationelement().updateElements(doc) + # If we are inserting, we shouldn't have any old elements, so don't + # bother removing them. + elements = doc['annotation'].pop('elements', None) + ret = insert_one(doc, *args, **kwargs) + if elements is not None: + doc['annotation']['elements'] = elements + return ret + + with self._writeLock: + self.collection.replace_one = replaceElements + self.collection.insert_one = insertElements + try: + result = super().save(annotation, *args, **kwargs) + finally: + self.collection.replace_one = replace_one + self.collection.insert_one = insert_one + if _elementQuery: + result['_elementQuery'] = _elementQuery + + annotation.pop('groups', None) + self.injectAnnotationGroupSet(annotation) + + if annotation['annotation'].get('elements') is not None: + logger.info( + 'Saved annotation %s in %5.3fs with %d element(s)', + annotation.get('_id', None), + time.time() - starttime, + len(annotation['annotation']['elements'])) + events.trigger('large_image.annotations.save_history', { + 'annotation': annotation, + }, asynchronous=True) + return result
+ + +
+[docs] + def updateAnnotation(self, annotation, updateUser=None): + """ + Update an annotation. + + :param annotation: the annotation document to update. + :param updateUser: the user who is creating the update. + :returns: the annotation document that was updated. + """ + annotation['updated'] = datetime.datetime.now(datetime.timezone.utc) + annotation['updatedId'] = updateUser['_id'] if updateUser else None + annotation = self.save(annotation) + Notification().createNotification( + type='large_image_annotation.update', + data={'_id': annotation['_id'], 'itemId': annotation['itemId']}, + user=User().load(annotation['creatorId'], force=True), + expires=datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1)) + return annotation
+ + + def _similarElementStructure(self, a, b, parentKey=None): # noqa + """ + Compare two elements to determine if they are similar enough that if + one validates, the other should, too. This is called recursively to + validate dictionaries. In general, types must be the same, + dictionaries must contain the same keys, arrays must be the same + length. The only differences that are allowed are numerical values may + be different, ids may be different, and point arrays may contain + different numbers of elements. + + :param a: first element + :param b: second element + :param parentKey: if set, the key of the dictionary that used for this + part of the comparison. + :returns: True if the elements are similar. False if they are not. + """ + # This function exceeds the recommended complexity, but since it is + # needs to be relatively fast, breaking it into smaller functions is + # probably undesirable. + if not isinstance(a, type(b)): + return False + if isinstance(a, dict): + if len(a) != len(b): + return False + for k in a: + if k not in b: + return False + if k == 'id': + if not isinstance(b[k], str) or not self.idRegex.match(b[k]): + return False + elif parentKey in {'user'} or k in {'fillColor', 'lineColor'}: + continue + elif parentKey != 'label' or k != 'value': + if not self._similarElementStructure(a[k], b[k], k): + return False + elif isinstance(a, list): + if parentKey == 'holes': + return all( + len(hole) == 3 and + # this is faster than checking the instance type, and, if + # it raises an exception, it would have failed validation + # any way. + 1 + hole[0] + hole[1] + hole[2] is not None + # isinstance(hole[0], self.numberInstance) and + # isinstance(hole[1], self.numberInstance) and + # isinstance(hole[2], self.numberInstance) + for hlist in b + for hole in hlist) + if len(a) != len(b): + if parentKey not in {'points', 'values'} or len(a) < 2 or len(b) < 2: + return False + # If this is an array of points, let it pass + return all( + len(elem) == 3 and + # this is faster than checking the instance type, and, if + # it raises an exception, it would have failed validation + # any way. + 1 + elem[0] + elem[1] + elem[2] is not None + # isinstance(elem[0], self.numberInstance) and + # isinstance(elem[1], self.numberInstance) and + # isinstance(elem[2], self.numberInstance) + for elem in b) + for idx in range(len(a)): + if not self._similarElementStructure(a[idx], b[idx], parentKey): + return False + elif not isinstance(a, self.numberInstance): + return a == b + # Either a number or the dictionary or list comparisons passed + return True + +
+[docs] + def validate(self, doc): # noqa + startTime = lastTime = time.time() + try: + # This block could just use the json validator: + # jsonschema.validate(doc.get('annotation'), + # AnnotationSchema.annotationSchema) + # but this is very slow. Instead, validate the main structure and + # then validate each element. If sequential elements are similar + # in structure, skip validating them. + annot = doc.get('annotation') + elements = annot.get('elements', []) + annot['elements'] = [] + self.validatorAnnotation.validate(annot) + lastValidatedElement = None + lastValidatedElement2 = None + for idx, element in enumerate(elements): + # Discard element keys beginning with _ + for key in list(element): + if key.startswith('_'): + del element[key] + if isinstance(element.get('id'), ObjectId): + element['id'] = str(element['id']) + # Handle elements with large arrays by checking that a + # conversion to a numpy array works + keys = None + if len(element.get('points', element.get('values', []))) > VALIDATE_ARRAY_LENGTH: + key = 'points' if 'points' in element else 'values' + try: + # Check if the entire array converts in an obvious + # manner + np.array(element[key], dtype=float) + keys[key] = element[key] + element[key] = element[key][:VALIDATE_ARRAY_LENGTH] + except Exception: + pass + if any(len(h) > VALIDATE_ARRAY_LENGTH for h in element.get('holes', [])): + key = 'holes' + try: + for h in element['holes']: + np.array(h, dtype=float) + keys[key] = element[key] + element[key] = [] + except Exception: + pass + try: + if (not self._similarElementStructure(element, lastValidatedElement) and + not self._similarElementStructure(element, lastValidatedElement2)): + self.validatorAnnotationElement.validate(element) + lastValidatedElement2 = lastValidatedElement + lastValidatedElement = element + except TypeError: + self.validatorAnnotationElement.validate(element) + if keys: + element.update(keys) + if time.time() - lastTime > 10: + logger.info('Validated %s of %d elements in %5.3fs', + idx + 1, len(elements), time.time() - startTime) + lastTime = time.time() + annot['elements'] = elements + except jsonschema.ValidationError as exp: + raise ValidationException(exp) + if time.time() - startTime > 10: + logger.info('Validated in %5.3fs' % (time.time() - startTime)) + elementIds = [entry['id'] for entry in + doc['annotation'].get('elements', []) if 'id' in entry] + if len(set(elementIds)) != len(elementIds): + msg = 'Annotation Element IDs are not unique' + raise ValidationException(msg) + return doc
+ + +
+[docs] + def versionList(self, annotationId, user=None, limit=0, offset=0, + sort=(('_version', -1), ), force=False): + """ + List annotation history entries for a specific annotationId. Only + annotations that belong to an existing item that the user is allowed to + view are included. If the user is an admin, all annotations will be + included. + + :param annotationId: the annotation to get history for. + :param user: the Girder user. + :param limit: maximum number of history entries to return. + :param offset: skip this many entries. + :param sort: the sort method used. Defaults to reverse _id. + :param force: if True, don't authenticate the user. + :yields: the entries in the list + """ + if annotationId and not isinstance(annotationId, ObjectId): + annotationId = ObjectId(annotationId) + # Make sure we have only one of each version, plus apply our filter and + # sort. Don't apply limit and offset here, as they are subject to + # access control and other effects + entries = self.collection.aggregate([ + {'$match': {'$or': [{'_id': annotationId}, {'_annotationId': annotationId}]}}, + {'$group': {'_id': '$_version', '_doc': {'$first': '$$ROOT'}}}, + {'$replaceRoot': {'newRoot': '$_doc'}}, + {'$sort': {s[0]: s[1] for s in sort}}]) + if not force: + entries = self.filterResultsByPermission( + cursor=entries, user=user, level=AccessType.READ, + limit=limit, offset=offset) + return entries
+ + +
+[docs] + def getVersion(self, annotationId, version, user=None, force=False, *args, **kwargs): + """ + Get an annotation history version. This reconstructs the original + annotation. + + :param annotationId: the annotation to get history for. + :param version: the specific version to get. + :param user: the Girder user. If the user is not an admin, they must + have read access on the item and the item must exist. + :param force: if True, don't get the user access. + """ + if annotationId and not isinstance(annotationId, ObjectId): + annotationId = ObjectId(annotationId) + entry = self.findOne({ + '$or': [{'_id': annotationId}, {'_annotationId': annotationId}], + '_version': int(version), + }, fields=['_id']) + if not entry: + return None + result = self.load(entry['_id'], *args, user=user, force=force, **kwargs) + result['_versionId'] = result['_id'] + result['_id'] = result.pop('annotationId', result['_id']) + return result
+ + +
+[docs] + def revertVersion(self, id, version=None, user=None, force=False): + """ + Revert to a previous version of an annotation. + + :param id: the annotation id. + :param version: the version to revert to. None reverts to the previous + version. If the annotation was deleted, this is the most recent + version. + :param user: the user doing the reversion. + :param force: if True don't authenticate the user with the associated + item access. + """ + if version is None: + oldVersions = list(Annotation().versionList(id, limit=2, force=True)) + if len(oldVersions) >= 1 and oldVersions[0].get('_active') is False: + version = oldVersions[0]['_version'] + elif len(oldVersions) >= 2: + version = oldVersions[1]['_version'] + annotation = Annotation().getVersion(id, version, user, force=force) + if annotation is None: + return + # If this is the most recent (active) annotation, don't do anything. + # Otherwise, revert it. + if not annotation.get('_active', True): + if not force: + self.requireAccess(annotation, user=user, level=AccessType.WRITE) + annotation = Annotation().updateAnnotation(annotation, updateUser=user) + return annotation
+ + +
+[docs] + def findAnnotatedImages(self, imageNameFilter=None, creator=None, + user=None, level=AccessType.ADMIN, force=None, + offset=0, limit=0, sort=None, **kwargs): + r""" + Find images associated with annotations. + + The list returned by this function is paginated and filtered by access control using + the standard girder kwargs. + + :param imageNameFilter: A string used to filter images by name. An image name matches + if it (or a subtoken) begins with this string. Subtokens are generated by splitting + by the regex ``[\W_]+`` This filter is case-insensitive. + :param creator: Filter by a user who is the creator of the annotation. + """ + query = {'_active': {'$ne': False}} + if creator: + query['creatorId'] = creator['_id'] + + annotations = self.find( + query, sort=sort, fields=['itemId']) + + images = [] + imageIds = set() + for annotation in annotations: + # short cut if the image has already been added to the results + if annotation['itemId'] in imageIds: + continue + + try: + item = ImageItem().load(annotation['itemId'], level=level, user=user, force=force) + except AccessException: + item = None + + # ignore if no such item exists + if not item: + continue + + if not self._matchImageName(item['name'], imageNameFilter or ''): + continue + + if len(imageIds) >= offset: + images.append(item) + + imageIds.add(item['_id']) + if len(images) == limit: + break + return images
+ + + def _matchImageName(self, imageName, matchString): + matchString = matchString.lower() + imageName = imageName.lower() + if imageName.startswith(matchString): + return True + tokens = re.split(r'[\W_]+', imageName, flags=re.UNICODE) + return any(token.startswith(matchString) for token in tokens) + +
+[docs] + def injectAnnotationGroupSet(self, annotation): + if 'groups' not in annotation: + annotation['groups'] = Annotationelement().getElementGroupSet(annotation) + query = { + '_id': ObjectId(annotation['_id']), + } + update = { + '$set': { + 'groups': annotation['groups'], + }, + } + self.collection.update_one(query, update) + return annotation
+ + +
+[docs] + def setAccessList(self, doc, access, save=False, **kwargs): + """ + The super class's setAccessList function can save a document. However, + annotations which have not loaded elements lose their elements when + this occurs, because the validation step of the save function adds an + empty element list. By using an update instead of a save, this + prevents the problem. + """ + update = save and '_id' in doc + save = save and '_id' not in doc + doc = super().setAccessList(doc, access, save=save, **kwargs) + if update: + self.update({'_id': doc['_id']}, {'$set': {'access': doc['access']}}) + return doc
+ + +
+[docs] + def removeOldAnnotations(self, remove=False, minAgeInDays=30, keepInactiveVersions=5): # noqa + """ + Remove annotations that (a) have no item or (b) are inactive and at + least (1) a minimum age in days and (2) not the most recent inactive + versions. Also remove any annotation elements that don't have + associated annotations and are a minimum age in days. + + :param remove: if False, just report on what would be done. If true, + actually remove the annotations and compact the collections. + :param minAgeInDays: only work on annotations that are at least this + old. This must be greater than or equal to 7. + :param keepInactiveVersions: keep at least this many inactive versions + of any annotation, regardless of age. + """ + if (remove and minAgeInDays < 7) or minAgeInDays < 0: + msg = 'minAgeInDays must be >= 7' + raise ValidationException(msg) + age = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(-minAgeInDays) + if keepInactiveVersions < 0: + msg = 'keepInactiveVersions mist be non-negative' + raise ValidationException(msg) + logger.info('Checking old annotations') + logtime = time.time() + report = {'fromDeletedItems': 0, 'oldVersions': 0, 'abandonedVersions': 0} + if remove: + report['removedVersions'] = 0 + report['active'] = self.collection.count_documents({'_active': {'$ne': False}}) + if time.time() - logtime > 10: + logger.info('Counting inactive annotations, %r' % report) + logtime = time.time() + report['recentVersions'] = self.collection.count_documents({'_active': False}) + recentDateStep = {'$addFields': {'mostRecentDate': {'$max': [ + '$created', {'$ifNull': ['$updated', '$created']}]}}} + itemLookupStep = {'$lookup': { + 'from': 'item', + 'localField': 'itemId', + 'foreignField': '_id', + 'as': 'item', + }} + oldDeletedPipeline = [recentDateStep, itemLookupStep, { + '$match': {'item': {'$size': 0}, 'mostRecentDate': {'$lt': age}}, + }, { + '$project': {'item': 0}, + }] + if time.time() - logtime > 10: + logger.info('Finding deleted annotations, %r' % report) + logtime = time.time() + for annot in self.collection.aggregate(oldDeletedPipeline): + if time.time() - logtime > 10: + logger.info('Checking deleted annotations, %r' % report) + logtime = time.time() + report['fromDeletedItems'] += 1 + if annot.get('_active', True): + report['active'] -= 1 + else: + report['recentVersions'] -= 1 + if remove: + self.collection.delete_one({'_id': annot['_id']}) + Annotationelement().removeWithQuery({'_version': annot['_version']}) + report['removedVersions'] += 1 + oldPipeline = [itemLookupStep, { + '$match': {'item': {'$ne': []}, '_active': False}, + }, { + '$group': {'_id': '$itemId', 'annotations': {'$push': '$$ROOT'}}, + }, { + '$project': {'annotations': {'$slice': [ + '$annotations', keepInactiveVersions, {'$size': '$annotations'}, + ]}}, + }, { + '$unwind': '$annotations', + }, { + '$replaceRoot': {'newRoot': '$annotations'}, + }, recentDateStep, { + '$match': {'mostRecentDate': {'$lt': age}}, + }] + if time.time() - logtime > 10: + logger.info('Finding old annotations, %r' % report) + logtime = time.time() + for annot in self.collection.aggregate(oldPipeline): + if time.time() - logtime > 10: + logger.info('Checking old annotations, %r' % report) + logtime = time.time() + report['oldVersions'] += 1 + report['recentVersions'] -= 1 + if remove: + self.collection.delete_one({'_id': annot['_id']}) + Annotationelement().removeWithQuery({'_version': annot['_version']}) + report['removedVersions'] += 1 + maxVersion = Annotationelement().getNextVersionValue() + oldElementPipeline = [{ + '$group': {'_id': '$_version'}, + }, { + '$lookup': { + 'from': 'annotation', + 'localField': '_id', + 'foreignField': '_version', + 'as': 'matchingAnnotations', + }, + }, { + '$match': {'matchingAnnotations': {'$eq': []}, '_id': {'$lt': maxVersion}}, + }] + if time.time() - logtime > 10: + logger.info('Finding abandoned versions, %r' % report) + logtime = time.time() + for row in Annotationelement().collection.aggregate(oldElementPipeline): + version = row['_id'] + if time.time() - logtime > 10: + logger.info('Removing abandoned versions, %r' % report) + logtime = time.time() + report['abandonedVersions'] += 1 + if remove: + Annotationelement().removeWithQuery({'_version': version}) + report['removedVersions'] += 1 + logger.info('Compacting annotation collection') + self.collection.database.command('compact', self.name) + logger.info('Compacting annotationelement collection') + self.collection.database.command('compact', Annotationelement().name) + logger.info('Done compacting collections') + logger.info('Finished checking old annotations, %r' % report) + return report
+ + +
+[docs] + def setMetadata(self, annotation, metadata, allowNull=False): + """ + Set metadata on an annotation. A `ValidationException` is thrown in + the cases where the metadata JSON object is badly formed, or if any of + the metadata keys contains a period ('.'). + + :param annotation: The annotation to set the metadata on. + :type annotation: dict + :param metadata: A dictionary containing key-value pairs to add to + the annotations meta field + :type metadata: dict + :param allowNull: Whether to allow `null` values to be set in the + annotation's metadata. If set to `False` or omitted, a `null` value + will cause that metadata field to be deleted. + :returns: the annotation document + """ + if 'attributes' not in annotation['annotation']: + annotation['annotation']['attributes'] = {} + + # Add new metadata to existing metadata + annotation['annotation']['attributes'].update(metadata.items()) + + # Remove metadata fields that were set to null + if not allowNull: + toDelete = [k for k, v in metadata.items() if v is None] + for key in toDelete: + del annotation['annotation']['attributes'][key] + + self.validateKeys(annotation['annotation']['attributes']) + + annotation['updated'] = datetime.datetime.now(datetime.timezone.utc) + + # Validate and save the annotation + return super().save(annotation)
+ + +
+[docs] + def deleteMetadata(self, annotation, fields): + """ + Delete metadata on an annotation. A `ValidationException` is thrown if + the metadata field names contain a period ('.') or begin with a dollar + sign ('$'). + + :param annotation: The annotation to delete metadata from. + :type annotation: dict + :param fields: An array containing the field names to delete from the + annotation's meta field + :type field: list + :returns: the annotation document + """ + self.validateKeys(fields) + + if 'attributes' not in annotation['annotation']: + annotation['annotation']['attributes'] = {} + + for field in fields: + annotation['annotation']['attributes'].pop(field, None) + + annotation['updated'] = datetime.datetime.now(datetime.timezone.utc) + + return super().save(annotation)
+ + +
+[docs] + def geojson(self, annotation): + """ + Yield an annotation as geojson generator. + + :param annotation: The annotation to delete metadata from. + :yields: geojson. General annotation properties are added to the first + feature under the annotation tag. + """ + yield from AnnotationGeoJSON(annotation['_id'])
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image_annotation/models/annotationelement.html b/_modules/girder_large_image_annotation/models/annotationelement.html new file mode 100644 index 000000000..fca761040 --- /dev/null +++ b/_modules/girder_large_image_annotation/models/annotationelement.html @@ -0,0 +1,815 @@ + + + + + + girder_large_image_annotation.models.annotationelement — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image_annotation.models.annotationelement

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import concurrent.futures
+import datetime
+import io
+import math
+import pickle
+import threading
+import time
+
+import pymongo
+from girder_large_image.models.image_item import ImageItem
+
+import large_image
+from girder import logger
+from girder.constants import AccessType, SortDir
+from girder.models.file import File
+from girder.models.item import Item
+from girder.models.model_base import Model
+from girder.models.upload import Upload
+
+# Some annotation elements can be very large.  If they pass a size threshold,
+# store part of them in an associated file.  This is slower, so don't do it for
+# small ones.
+MAX_ELEMENT_CHECK = 100
+MAX_ELEMENT_DOCUMENT = 10000
+MAX_ELEMENT_USER_DOCUMENT = 1000000
+
+
+
+[docs] +class Annotationelement(Model): + bboxKeys = { + 'left': ('bbox.highx', '$gte'), + 'right': ('bbox.lowx', '$lt'), + 'top': ('bbox.highy', '$gte'), + 'bottom': ('bbox.lowy', '$lt'), + 'low': ('bbox.highz', '$gte'), + 'high': ('bbox.lowz', '$lt'), + 'minimumSize': ('bbox.size', '$gte'), + 'size': ('bbox.size', None), + 'details': ('bbox.details', None), + } + +
+[docs] + def initialize(self): + self.name = 'annotationelement' + self.ensureIndices([ + 'annotationId', + '_version', + ([ + ('annotationId', SortDir.ASCENDING), + ('bbox.lowx', SortDir.DESCENDING), + ('bbox.highx', SortDir.ASCENDING), + ('bbox.size', SortDir.DESCENDING), + ], { + 'name': 'annotationBboxIdx', + }), + ([ + ('annotationId', SortDir.ASCENDING), + ('bbox.size', SortDir.DESCENDING), + ], { + 'name': 'annotationBboxSizeIdx', + }), + ([ + ('annotationId', SortDir.ASCENDING), + ('_version', SortDir.DESCENDING), + ('element.group', SortDir.ASCENDING), + ], { + 'name': 'annotationGroupIdx', + }), + ([ + ('annotationId', SortDir.ASCENDING), + ('_version', SortDir.DESCENDING), + ('_id', SortDir.ASCENDING), + ], { + 'name': 'annotationElementIdIdx', + }), + ([ + ('created', SortDir.ASCENDING), + ('_version', SortDir.ASCENDING), + ], {}), + 'element.girderId', + ]) + + self.exposeFields(AccessType.READ, ( + '_id', '_version', 'annotationId', 'created', 'element')) + self.versionId = None
+ + +
+[docs] + def getNextVersionValue(self): + """ + Maintain a version number. This is a single sequence that can be used + to ensure we have the correct set of elements for an annotation. + + :returns: an integer version number that is strictly increasing. + """ + version = None + if self.versionId is not None: + version = self.collection.find_one_and_update( + {'_id': self.versionId}, + {'$inc': {'_version': 1}}) + if version is None: + versionObject = self.collection.find_one( + {'annotationId': 'version_sequence'}) + if versionObject is None: + startingId = self.collection.find_one({}, sort=[('_version', SortDir.DESCENDING)]) + startingId = startingId['_version'] + 1 if startingId else 0 + self.versionId = self.collection.insert_one( + {'annotationId': 'version_sequence', '_version': startingId}, + ).inserted_id + else: + self.versionId = versionObject['_id'] + version = self.collection.find_one_and_update( + {'_id': self.versionId}, + {'$inc': {'_version': 1}}) + return version['_version']
+ + +
+[docs] + def getElements(self, annotation, region=None): + """ + Given an annotation, fetch the elements from the database and add them + to it. + + When a region is used to request specific element, the following + keys can be specified: + + :left, right, top, bottom, low, high: the spatial area where + elements are located, all in pixels. If an element's bounding + box is at least partially within the requested area, that + element is included. + :minimumSize: the minimum size of an element to return. + :sort, sortdir: standard sort options. The sort key can include + size and details. + :limit: limit the total number of elements by this value. Defaults + to no limit. + :offset: the offset within the query to start returning values. If + maxDetails is used, to get subsequent sets of elements, the + offset needs to be increased by the actual number of elements + returned from a previous query, which will vary based on the + details of the elements. + :maxDetails: if specified, limit the total number of elements by + the sum of their details values. This is applied in addition + to limit. The sum of the details values of the elements may + exceed maxDetails slightly (the sum of all but the last element + will be less than maxDetails, but the last element may exceed + the value). + :minElements: if maxDetails is specified, always return this many + elements even if they are very detailed. + :centroids: if specified and true, only return the id, center of + the bounding box, and bounding box size for each element. + + :param annotation: the annotation to get elements for. Modified. + :param region: if present, a dictionary restricting which annotations + are returned. + """ + annotation['_elementQuery'] = {} + annotation['annotation']['elements'] = list(self.yieldElements( + annotation, region, annotation['_elementQuery']))
+ + +
+[docs] + def countElements(self, annotation): + query = { + 'annotationId': annotation.get('_annotationId', annotation['_id']), + '_version': annotation['_version'], + } + return self.collection.count_documents(query)
+ + +
+[docs] + def yieldElements(self, annotation, region=None, info=None, bbox=False): # noqa + """ + Given an annotation, fetch the elements from the database. + + When a region is used to request specific element, the following + keys can be specified: + + :left, right, top, bottom, low, high: the spatial area where + elements are located, all in pixels. If an element's bounding + box is at least partially within the requested area, that + element is included. + :minimumSize: the minimum size of an element to return. + :sort, sortdir: standard sort options. The sort key can include + size and details. + :limit: limit the total number of elements by this value. Defaults + to no limit. + :offset: the offset within the query to start returning values. If + maxDetails is used, to get subsequent sets of elements, the + offset needs to be increased by the actual number of elements + returned from a previous query, which will vary based on the + details of the elements. + :maxDetails: if specified, limit the total number of elements by + the sum of their details values. This is applied in addition + to limit. The sum of the details values of the elements may + exceed maxDetails slightly (the sum of all but the last element + will be less than maxDetails, but the last element may exceed + the value). + :minElements: if maxDetails is specified, always return this many + elements even if they are very detailed. + :centroids: if specified and true, only return the id, center of + the bounding box, and bounding box size for each element. + :bbox: if specified and true and centroids are not specified, + add _bbox to each element with the bounding box record. + + :param annotation: the annotation to get elements for. Modified. + :param region: if present, a dictionary restricting which annotations + are returned. + :param info: an optional dictionary that will be modified with + additional query information, including count (total number of + available elements), returned (number of elements in response), + maxDetails (as specified by the region dictionary), details (sum of + details returned), limit (as specified by region), centroids (a + boolean based on the region specification). + :param bbox: if True, always return bounding box information. + :returns: a list of elements. If centroids were requested, each entry + is a list with str(id), x, y, size. Otherwise, each entry is the + element record. + """ + info = info if info is not None else {} + region = region or {} + query = { + 'annotationId': annotation.get('_annotationId', annotation['_id']), + '_version': annotation['_version'], + } + for key in region: + if key in self.bboxKeys and self.bboxKeys[key][1]: + if self.bboxKeys[key][1] == '$gte' and float(region[key]) <= 0: + continue + query[self.bboxKeys[key][0]] = { + self.bboxKeys[key][1]: float(region[key])} + if region.get('sort') in self.bboxKeys: + sortkey = self.bboxKeys[region['sort']][0] + else: + sortkey = region.get('sort') or '_id' + sortdir = int(region['sortdir']) if region.get('sortdir') else SortDir.ASCENDING + limit = int(region['limit']) if region.get('limit') else 0 + maxDetails = int(region.get('maxDetails') or 0) + minElements = int(region.get('minElements') or 0) + queryLimit = max(minElements, maxDetails) if maxDetails and ( + not limit or max(minElements, maxDetails) < limit) else limit + offset = int(region['offset']) if region.get('offset') else 0 + logger.debug('element query %r for %r', query, region) + fields = {'_id': True, 'element': True, 'bbox.details': True, 'datafile': True} + centroids = str(region.get('centroids')).lower() == 'true' + if centroids: + # fields = {'_id': True, 'element': True, 'bbox': True} + fields = { + '_id': True, + 'element.id': True, + 'bbox': True} + proplist = [] + propskeys = ['type', 'fillColor', 'lineColor', 'lineWidth', 'closed'] + # This should match the javascript + defaultProps = { + 'fillColor': 'rgba(0,0,0,0)', + 'lineColor': 'rgb(0,0,0)', + 'lineWidth': 2, + } + for key in propskeys: + fields['element.%s' % key] = True + props = {} + info['centroids'] = True + info['props'] = proplist + info['propskeys'] = propskeys + elif region.get('bbox'): + fields.pop('bbox.details') + fields['bbox'] = True + if bbox: + fields.pop('bbox.details', None) + fields['bbox'] = True + elementCursor = self.find( + query=query, sort=[(sortkey, sortdir)], limit=queryLimit, + offset=offset, fields=fields) + + info.update({ + 'count': elementCursor.count(), + 'offset': offset, + 'filter': query, + 'sort': [sortkey, sortdir], + }) + details = count = 0 + if maxDetails: + info['maxDetails'] = maxDetails + if minElements: + info['minElements'] = minElements + if limit: + info['limit'] = limit + for entry in elementCursor: + element = entry['element'] + element.setdefault('id', entry['_id']) + if centroids: + bbox = entry.get('bbox') + if not bbox or 'lowx' not in bbox or 'size' not in bbox: + continue + prop = tuple( + element.get(key, defaultProps.get(key)) for key in propskeys + if element.get(key, defaultProps.get(key)) is not None) + if prop not in props: + props[prop] = len(props) + proplist.append(list(prop)) + yield [ + str(element['id']), + (bbox['lowx'] + bbox['highx']) / 2, + (bbox['lowy'] + bbox['highy']) / 2, + bbox['size'] if entry.get('type') != 'point' else 0, + props[prop], + ] + details += 1 + else: + if entry.get('datafile'): + datafile = entry['datafile'] + data = io.BytesIO() + chunksize = 1024 ** 2 + with File().open(File().load(datafile['fileId'], force=True)) as fptr: + while True: + chunk = fptr.read(chunksize) + if not len(chunk): + break + data.write(chunk) + data.seek(0) + element[datafile['key']] = pickle.load(data) + if 'userFileId' in datafile: + data = io.BytesIO() + chunksize = 1024 ** 2 + with File().open(File().load(datafile['userFileId'], force=True)) as fptr: + while True: + chunk = fptr.read(chunksize) + if not len(chunk): + break + data.write(chunk) + data.seek(0) + element['user'] = pickle.load(data) + if region.get('bbox') and 'bbox' in entry: + element['_bbox'] = entry['bbox'] + if 'bbox' not in info: + info['bbox'] = {} + for axis in {'x', 'y', 'z'}: + lkey, hkey = 'low' + axis, 'high' + axis + if lkey in entry['bbox'] and hkey in entry['bbox']: + info['bbox'][lkey] = min( + info['bbox'].get(lkey, entry['bbox'][lkey]), entry['bbox'][lkey]) + info['bbox'][hkey] = max( + info['bbox'].get(hkey, entry['bbox'][hkey]), entry['bbox'][hkey]) + elif bbox and 'bbox' in entry: + element['_bbox'] = entry['bbox'] + yield element + details += entry.get('bbox', {}).get('details', 1) + count += 1 + if maxDetails and details >= maxDetails and count >= minElements: + break + info['returned'] = count + info['details'] = details
+ + +
+[docs] + def removeWithQuery(self, query): + """ + Remove all documents matching a given query from the collection. + For safety reasons, you may not pass an empty query. + + Note: this does NOT return a Mongo DeleteResult. + + :param query: The search query for documents to delete, + see general MongoDB docs for "find()" + :type query: dict + """ + if not query: + msg = 'query must be specified' + raise Exception(msg) + + attachedQuery = query.copy() + attachedQuery['datafile'] = {'$exists': True} + for element in self.collection.find(attachedQuery): + for key in {'fileId', 'userFileId'}: + if key in element['datafile']: + file = File().load(element['datafile'][key], force=True) + if file: + File().remove(file) + self.collection.bulk_write([pymongo.DeleteMany(query)], ordered=False)
+ + +
+[docs] + def removeElements(self, annotation): + """ + Remove all elements related to the specified annotation. + + :param annotation: the annotation to remove elements from. + """ + self.removeWithQuery({'annotationId': annotation['_id']})
+ + +
+[docs] + def removeOldElements(self, annotation, oldversion=None): + """ + Remove all elements related to the specified annotation. + + :param annotation: the annotation to remove elements from. + :param oldversion: if present, remove versions up to this number. If + none, remove versions earlier than the version in + the annotation record. + """ + query = {'annotationId': annotation['_id']} + if oldversion is None or oldversion >= annotation['_version']: + query['_version'] = {'$lt': annotation['_version']} + else: + query['_version'] = {'$lte': oldversion} + self.removeWithQuery(query)
+ + + def _overlayBounds(self, overlayElement): + """ + Compute bounding box information in the X-Y plane for an + image overlay element. + + This uses numpy to perform the specified transform on the given girder + image item in order to obtain bounding box coordinates. + + :param overlayElement: An annotation element of type 'image'. + :returns: a tuple with 4 values: lowx, highx, lowy, highy. Runtime exceptions + during loading the image metadata will result in the tuple (0, 0, 0, 0). + """ + if overlayElement.get('type') not in ['image', 'pixelmap']: + msg = ('Function _overlayBounds only accepts annotation elements ' + 'of type "image", "pixelmap."') + raise ValueError(msg) + + import numpy as np + lowx = highx = lowy = highy = 0 + + try: + overlayItemId = overlayElement.get('girderId') + imageItem = ImageItem().load(overlayItemId, force=True) + overlayImageMetadata = ImageItem().getMetadata(imageItem) + corners = [ + [0, 0], + [0, overlayImageMetadata['sizeY']], + [overlayImageMetadata['sizeX'], overlayImageMetadata['sizeY']], + [overlayImageMetadata['sizeX'], 0], + ] + transform = overlayElement.get('transform', {}) + transformMatrix = np.array(transform.get('matrix', [[1, 0], [0, 1]])) + corners = [np.matmul(np.array(corner), transformMatrix) for corner in corners] + offsetArray = np.array([transform.get('xoffset', 0), transform.get('yoffset', 0)]) + corners = [np.add(corner, offsetArray) for corner in corners] + # use .item() to convert back to native python types + lowx = min([corner[0] for corner in corners]).item() + highx = max([corner[0] for corner in corners]).item() + lowy = min([corner[1] for corner in corners]).item() + highy = max([corner[1] for corner in corners]).item() + except Exception: + logger.exception('Error generating bounding box for image overlay annotation') + return lowx, highx, lowy, highy + + def _boundingBox(self, element): + """ + Compute bounding box information for an annotation element. + + This computes the enclosing bounding box of an element. For points, an + small non-zero-area region is used centered on the point. + Additionally, a metric is stored for the complexity of the element. + The size of the bounding box's x-y diagonal is also stored. + + :param element: the element to compute the bounding box for. + :returns: the bounding box dictionary. This contains 'lowx', 'lowy', + 'lowz', 'highx', 'highy', and 'highz, which are the minimum and + maximum values in each dimension, 'details' with the complexity of + the element, and 'size' with the x-y diagonal size of the bounding + box. + """ + bbox = {} + if 'points' in element: + pts = element['points'] + p0 = [p[0] for p in pts] + p1 = [p[1] for p in pts] + p2 = [p[2] for p in pts] + bbox['lowx'] = min(p0) + bbox['lowy'] = min(p1) + bbox['lowz'] = min(p2) + bbox['highx'] = max(p0) + bbox['highy'] = max(p1) + bbox['highz'] = max(p2) + bbox['details'] = len(pts) + elif element.get('type') == 'griddata': + x0, y0, z = element['origin'] + isElements = element.get('interpretation') == 'choropleth' + x1 = x0 + element['dx'] * (element['gridWidth'] - (1 if not isElements else 0)) + y1 = y0 + element['dy'] * (math.ceil(len(element['values']) / element['gridWidth']) - + (1 if not isElements else 0)) + bbox['lowx'] = min(x0, x1) + bbox['lowy'] = min(y0, y1) + bbox['lowz'] = bbox['highz'] = z + bbox['highx'] = max(x0, x1) + bbox['highy'] = max(y0, y1) + bbox['details'] = len(element['values']) + elif element.get('type') in ['image', 'pixelmap']: + lowx, highx, lowy, highy = Annotationelement()._overlayBounds(element) + bbox['lowz'] = bbox['highz'] = 0 + bbox['lowx'] = lowx + bbox['highx'] = highx + bbox['lowy'] = lowy + bbox['highy'] = highy + bbox['details'] = 1 + else: + center = element['center'] + bbox['lowz'] = bbox['highz'] = center[2] + if 'width' in element: + w = element['width'] * 0.5 + h = element['height'] * 0.5 + if element.get('rotation'): + absin = abs(math.sin(element['rotation'])) + abcos = abs(math.cos(element['rotation'])) + w, h = max(abcos * w, absin * h), max(absin * w, abcos * h) + bbox['lowx'] = center[0] - w + bbox['lowy'] = center[1] - h + bbox['highx'] = center[0] + w + bbox['highy'] = center[1] + h + bbox['details'] = 4 + elif 'radius' in element: + rad = element['radius'] + bbox['lowx'] = center[0] - rad + bbox['lowy'] = center[1] - rad + bbox['highx'] = center[0] + rad + bbox['highy'] = center[1] + rad + bbox['details'] = 4 + else: + # This is a fall back for points. Although they have no + # dimension, make the bounding box have some extent. + bbox['lowx'] = center[0] - 0.5 + bbox['lowy'] = center[1] - 0.5 + bbox['highx'] = center[0] + 0.5 + bbox['highy'] = center[1] + 0.5 + bbox['details'] = 1 + bbox['size'] = ( + (bbox['highy'] - bbox['lowy'])**2 + + (bbox['highx'] - bbox['lowx'])**2) ** 0.5 + # we may want to store perimeter or area as that could help when we + # simplify to points + return bbox + + def _entryIsLarge(self, entry): + """ + Return True is an entry is alrge enough it might not fit in a mongo + document. + + :param entry: the entry to check. + :returns: True if the entry is large. + """ + if len(entry['element'].get('points', entry['element'].get( + 'values', []))) > MAX_ELEMENT_DOCUMENT: + return True + if ('user' in entry['element'] and + len(pickle.dumps(entry['element'], protocol=4)) > MAX_ELEMENT_USER_DOCUMENT): + return True + return False + +
+[docs] + def saveElementAsFile(self, annotation, entries): + """ + If an element has a large points or values array, save that array to an + attached file. + + :param annotation: the parent annotation. + :param entries: the database entries document. Modified. + """ + item = Item().load(annotation['itemId'], force=True) + for idx, entry in enumerate(entries[:MAX_ELEMENT_CHECK]): + if not self._entryIsLarge(entry): + continue + element = entry['element'].copy() + entries[idx]['element'] = element + key = 'points' if 'points' in element else 'values' + # Use the highest protocol support by all python versions we + # support + data = pickle.dumps(element.pop(key), protocol=4) + elementFile = Upload().uploadFromFile( + io.BytesIO(data), size=len(data), name='_annotationElementData', + parentType='item', parent=item, user=None, + mimeType='application/json', attachParent=True) + userdata = None + if 'user' in element: + userdata = pickle.dumps(element.pop('user'), protocol=4) + userFile = Upload().uploadFromFile( + io.BytesIO(userdata), size=len(userdata), name='_annotationElementUserData', + parentType='item', parent=item, user=None, + mimeType='application/json', attachParent=True) + entry['datafile'] = { + 'key': key, + 'fileId': elementFile['_id'], + } + if userdata: + entry['datafile']['userFileId'] = userFile['_id'] + logger.debug('Storing element as file (%r)', entry)
+ + +
+[docs] + def updateElementChunk(self, elements, chunk, chunkSize, annotation, now, insertLock): + """ + Update the database for a chunk of elements. See the updateElements + method for details. + """ + lastTime = time.time() + chunkStartTime = time.time() + entries = [{ + 'annotationId': annotation['_id'], + '_version': annotation['_version'], + 'created': now, + 'bbox': self._boundingBox(element), + 'element': element, + } for element in elements[chunk:chunk + chunkSize]] + prepTime = time.time() - chunkStartTime + if (len(entries) <= MAX_ELEMENT_CHECK and any( + self._entryIsLarge(entry) for entry in entries[:MAX_ELEMENT_CHECK])): + self.saveElementAsFile(annotation, entries) + with insertLock: + res = self.collection.insert_many(entries, ordered=False) + for pos, entry in enumerate(entries): + if 'id' not in entry['element']: + entry['element']['id'] = str(res.inserted_ids[pos]) + # If the insert is slow, log information about it. + if time.time() - lastTime > 10: + logger.info('insert %d elements in %4.2fs (prep time %4.2fs), chunk %d/%d' % ( + len(entries), time.time() - chunkStartTime, prepTime, + chunk + len(entries), len(elements))) + lastTime = time.time()
+ + +
+[docs] + def updateElements(self, annotation): + """ + Given an annotation, extract the elements from it and update the + database of them. + + :param annotation: the annotation to save elements for. Modified. + """ + startTime = time.time() + elements = annotation['annotation'].get('elements', []) + if not len(elements): + return + now = datetime.datetime.now(datetime.timezone.utc) + threads = large_image.config.cpu_count() + chunkSize = int(max(100000 // threads, 10000)) + insertLock = threading.Lock() + with concurrent.futures.ThreadPoolExecutor(max_workers=threads) as pool: + for chunk in range(0, len(elements), chunkSize): + pool.submit(self.updateElementChunk, elements, chunk, + chunkSize, annotation, now, insertLock) + if time.time() - startTime > 10: + logger.info('inserted %d elements in %4.2fs' % ( + len(elements), time.time() - startTime))
+ + +
+[docs] + def getElementGroupSet(self, annotation): + query = { + 'annotationId': annotation.get('_annotationId', annotation['_id']), + '_version': annotation['_version'], + } + groups = sorted([ + group for group in self.collection.distinct('element.group', filter=query) + if isinstance(group, str) + ]) + query['element.group'] = None + if self.collection.find_one(query): + groups.append(None) + return groups
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image_annotation/rest/annotation.html b/_modules/girder_large_image_annotation/rest/annotation.html new file mode 100644 index 000000000..9dba8e759 --- /dev/null +++ b/_modules/girder_large_image_annotation/rest/annotation.html @@ -0,0 +1,1172 @@ + + + + + + girder_large_image_annotation.rest.annotation — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image_annotation.rest.annotation

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import json
+import struct
+import time
+
+import cherrypy
+import orjson
+from bson.objectid import ObjectId
+from girder_large_image.rest.tiles import _handleETag
+
+from girder import logger
+from girder.api import access
+from girder.api.describe import Description, autoDescribeRoute, describeRoute
+from girder.api.rest import Resource, filtermodel, loadmodel, setResponseHeader
+from girder.constants import AccessType, SortDir, TokenScope
+from girder.exceptions import AccessException, RestException, ValidationException
+from girder.models.folder import Folder
+from girder.models.item import Item
+from girder.models.user import User
+from girder.utility import JsonEncoder
+from girder.utility.progress import setResponseTimeLimit
+
+from .. import constants, utils
+from ..models.annotation import Annotation, AnnotationSchema
+from ..models.annotationelement import Annotationelement
+
+
+
+[docs] +class AnnotationResource(Resource): + + def __init__(self): + super().__init__() + + self.resourceName = 'annotation' + self.route('GET', (), self.find) + self.route('POST', (), self.createAnnotation) + self.route('GET', ('schema',), self.getAnnotationSchema) + self.route('GET', ('images',), self.findAnnotatedImages) + self.route('GET', (':id',), self.getAnnotation) + self.route('GET', (':id', ':format'), self.getAnnotationWithFormat) + self.route('PUT', (':id',), self.updateAnnotation) + self.route('DELETE', (':id',), self.deleteAnnotation) + self.route('GET', (':id', 'access'), self.getAnnotationAccess) + self.route('PUT', (':id', 'access'), self.updateAnnotationAccess) + self.route('POST', (':id', 'copy'), self.copyAnnotation) + self.route('GET', (':id', 'history'), self.getAnnotationHistoryList) + self.route('GET', (':id', 'history', ':version'), self.getAnnotationHistory) + self.route('PUT', (':id', 'history', 'revert'), self.revertAnnotationHistory) + self.route('PUT', (':id', 'metadata'), self.setMetadata) + self.route('DELETE', (':id', 'metadata'), self.deleteMetadata) + self.route('GET', ('item', ':id'), self.getItemAnnotations) + self.route('POST', ('item', ':id'), self.createItemAnnotations) + self.route('DELETE', ('item', ':id'), self.deleteItemAnnotations) + self.route('POST', ('item', ':id', 'plot', 'list'), self.getItemPlottableElements) + self.route('POST', ('item', ':id', 'plot', 'data'), self.getItemPlottableData) + self.route('GET', ('folder', ':id'), self.returnFolderAnnotations) + self.route('GET', ('folder', ':id', 'present'), self.existFolderAnnotations) + self.route('GET', ('folder', ':id', 'create'), self.canCreateFolderAnnotations) + self.route('PUT', ('folder', ':id', 'access'), self.setFolderAnnotationAccess) + self.route('DELETE', ('folder', ':id'), self.deleteFolderAnnotations) + self.route('GET', ('counts',), self.getItemListAnnotationCounts) + self.route('GET', ('old',), self.getOldAnnotations) + self.route('DELETE', ('old',), self.deleteOldAnnotations) + +
+[docs] + @describeRoute( + Description('Search for annotations.') + .responseClass('Annotation') + .param('itemId', 'List all annotations in this item.', required=False) + .param('userId', 'List all annotations created by this user.', + required=False) + .param('text', 'Pass this to perform a full text search for ' + 'annotation names and descriptions.', required=False) + .param('name', 'Pass to lookup an annotation by exact name match.', + required=False) + .pagingParams(defaultSort='lowerName') + .errorResponse() + .errorResponse('Read access was denied on the parent item.', 403), + ) + @access.public(scope=TokenScope.DATA_READ) + @filtermodel(model='annotation', plugin='large_image') + def find(self, params): + limit, offset, sort = self.getPagingParameters(params, 'lowerName') + if sort and sort[0][0][0] == '[': + sort = json.loads(sort[0][0]) + query = {'_active': {'$ne': False}} + if 'itemId' in params: + item = Item().load(params.get('itemId'), force=True) + Item().requireAccess( + item, user=self.getCurrentUser(), level=AccessType.READ) + query['itemId'] = item['_id'] + if 'userId' in params: + user = User().load( + params.get('userId'), user=self.getCurrentUser(), + level=AccessType.READ) + query['creatorId'] = user['_id'] + if params.get('text'): + query['$text'] = {'$search': params['text']} + if params.get('name'): + query['annotation.name'] = params['name'] + fields = list( + ( + 'annotation.name', 'annotation.description', + 'annotation.attributes', 'annotation.display', + 'access', 'groups', '_version', + ) + Annotation().baseFields) + return Annotation().findWithPermissions( + query, sort=sort, fields=fields, user=self.getCurrentUser(), + level=AccessType.READ, limit=limit, offset=offset)
+ + +
+[docs] + @describeRoute( + Description('Get the official Annotation schema') + .notes('In addition to the schema, if IDs are specified on elements, ' + 'all IDs must be unique.') + .errorResponse(), + ) + @access.public(scope=TokenScope.DATA_READ) + def getAnnotationSchema(self, params): + return AnnotationSchema.annotationSchema
+ + +
+[docs] + @describeRoute( + Description('Get an annotation by id.') + .param('id', 'The ID of the annotation.', paramType='path') + .param('left', 'The left column of the area to fetch.', + required=False, dataType='float') + .param('right', 'The right column (exclusive) of the area to fetch.', + required=False, dataType='float') + .param('top', 'The top row of the area to fetch.', + required=False, dataType='float') + .param('bottom', 'The bottom row (exclusive) of the area to fetch.', + required=False, dataType='float') + .param('low', 'The lowest z value of the area to fetch.', + required=False, dataType='float') + .param('high', 'The highest z value (exclusive) of the area to fetch.', + required=False, dataType='float') + .param('minimumSize', 'Only annotations larger than or equal to this ' + 'size in pixels will be returned. Size is determined by the ' + 'length of the diagonal of the bounding box of an element. ' + 'This probably should be 1 at the maximum zoom, 2 at the next ' + 'level down, 4 at the next, etc.', required=False, + dataType='float') + .param('maxDetails', 'Limit the number of annotations returned based ' + 'on complexity. The complexity of an annotation is how many ' + 'points are used to defined it. This is applied in addition ' + 'to the limit. Using maxDetails helps ensure results will be ' + 'able to be rendered.', required=False, dataType='int') + .param('minElements', 'If maxDetails is specified, always return at ' + 'least this many elements, even if they are very detailed.', + required=False, dataType='int') + .param('centroids', 'If true, only return the centroids of each ' + 'element. The results are returned as a packed binary array ' + 'with a json wrapper.', dataType='boolean', required=False) + .param('bbox', 'If true, add _bbox records to each element. These ' + 'are computed when the annotation is stored and cannot be ' + 'modified. Cannot be used with the centroids option.', + dataType='boolean', required=False) + .pagingParams(defaultSort='_id', defaultLimit=None, + defaultSortDir=SortDir.ASCENDING) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the annotation.', 403) + .notes('Use "size" or "details" as possible sort keys.'), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + def getAnnotation(self, id, params): + user = self.getCurrentUser() + annotation = Annotation().load( + id, region=params, user=user, level=AccessType.READ, getElements=False) + _handleETag('getAnnotation', annotation, params, max_age=86400 * 30) + if annotation is None: + msg = 'Annotation not found' + raise RestException(msg, 404) + return self._getAnnotation(annotation, params)
+ + +
+[docs] + @autoDescribeRoute( + Description('Get an annotation by id in a specific format.') + .param('id', 'The ID of the annotation.', paramType='path') + .param('format', 'The format of the annotation.', paramType='path', + enum=['geojson']) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the annotation.', 403) + .notes('Use "size" or "details" as possible sort keys.'), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + @loadmodel(model='annotation', plugin='large_image', getElements=False, level=AccessType.READ) + def getAnnotationWithFormat(self, annotation, format): + _handleETag('getAnnotationWithFormat', annotation, format, max_age=86400 * 30) + if annotation is None: + msg = 'Annotation not found' + raise RestException(msg, 404) + + def generateResult(): + for chunk in Annotation().geojson(annotation): + yield chunk.encode() + + setResponseHeader('Content-Type', 'application/json') + return generateResult
+ + + def _getAnnotation(self, annotation, params): + """ + Get a generator function that will yield the json of an annotation. + + :param annotation: the annotation document without elements. + :param params: paging and region parameters for the annotation. + :returns: a function that will return a generator. + """ + # Set the response time limit to a very long value + setResponseTimeLimit(86400) + # Ensure that we have read access to the parent item. We could fail + # faster when there are permissions issues if we didn't load the + # annotation elements before checking the item access permissions. + # This had been done via the filtermodel decorator, but that doesn't + # work with yielding the elements one at a time. + annotation = Annotation().filter(annotation, self.getCurrentUser()) + + annotation['annotation']['elements'] = [] + breakStr = b'"elements": [' + base = json.dumps(annotation, sort_keys=True, allow_nan=False, + cls=JsonEncoder).encode('utf8').split(breakStr) + centroids = str(params.get('centroids')).lower() == 'true' + + def generateResult(): + info = {} + idx = 0 + yield base[0] + yield breakStr + collect = [] + if centroids: + # Add a null byte to indicate the start of the binary data + yield b'\x00' + for element in Annotationelement().yieldElements(annotation, params, info): + # The json conversion is fastest if we use defaults as much as + # possible. The only value in an annotation element that needs + # special handling is the id, so cast that ourselves and then + # use a json encoder in the most compact form. + if isinstance(element, dict): + element['id'] = str(element['id']) + else: + element = struct.pack( + '>QL', int(element[0][:16], 16), int(element[0][16:24], 16), + ) + struct.pack('<fffl', *element[1:]) + # Use orjson; it is much faster. The standard json library + # could be used in its most default mode instead like so: + # result = json.dumps(element, separators=(',', ':')) + # Collect multiple elements before emitting them. This + # balances using less memoryand streaming right away with + # efficiency in dumping the json. Experimentally, 100 is + # significantly faster than 10 and not much slower than 1000. + collect.append(element) + if len(collect) >= 100: + if isinstance(collect[0], dict): + # if switching json libraries, this may need + # json.dumps(collect).encode() + yield (b',' if idx else b'') + orjson.dumps(collect)[1:-1] + else: + yield b''.join(collect) + idx += 1 + collect = [] + if len(collect): + if isinstance(collect[0], dict): + # if switching json libraries, this may need + # json.dumps(collect).encode() + yield (b',' if idx else b'') + orjson.dumps(collect)[1:-1] + else: + yield b''.join(collect) + if centroids: + # Add a final null byte to indicate the end of the binary data + yield b'\x00' + yield base[1].rstrip().rstrip(b'}') + yield b', "_elementQuery": ' + yield json.dumps( + info, sort_keys=True, allow_nan=False, cls=JsonEncoder).encode('utf8') + yield b'}' + + if centroids: + setResponseHeader('Content-Type', 'application/octet-stream') + else: + setResponseHeader('Content-Type', 'application/json') + return generateResult + +
+[docs] + @describeRoute( + Description('Create an annotation.') + .responseClass('Annotation') + .param('itemId', 'The ID of the associated item.') + .param('body', 'A JSON object containing the annotation.', + paramType='body') + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403) + .errorResponse('Invalid JSON passed in request body.') + .errorResponse("Validation Error: JSON doesn't follow schema."), + ) + @access.user(scope=TokenScope.DATA_WRITE) + @loadmodel(map={'itemId': 'item'}, model='item', level=AccessType.READ) + @filtermodel(model='annotation', plugin='large_image') + def createAnnotation(self, item, params): + user = self.getCurrentUser() + folder = Folder().load(id=item['folderId'], user=user, level=AccessType.READ) + if Folder().hasAccess(folder, user, AccessType.WRITE) or Folder( + ).hasAccessFlags(folder, user, constants.ANNOTATION_ACCESS_FLAG): + try: + return Annotation().createAnnotation( + item, self.getCurrentUser(), self.getBodyJson()) + except ValidationException as exc: + logger.exception('Failed to validate annotation') + raise RestException( + "Validation Error: JSON doesn't follow schema (%r)." % ( + exc.args, )) + else: + msg = 'Write access and annotation creation access were denied for the item.' + raise RestException(msg, code=403)
+ + +
+[docs] + @describeRoute( + Description('Copy an annotation from one item to an other.') + .param('id', 'The ID of the annotation.', paramType='path', + required=True) + .param('itemId', 'The ID of the destination item.', + required=True) + .errorResponse('ID was invalid.') + .errorResponse('Write access was denied for the item.', 403), + ) + @access.user(scope=TokenScope.DATA_WRITE) + @loadmodel(model='annotation', plugin='large_image', level=AccessType.READ) + @filtermodel(model='annotation', plugin='large_image') + def copyAnnotation(self, annotation, params): + itemId = params['itemId'] + user = self.getCurrentUser() + Item().load(annotation.get('itemId'), + user=user, + level=AccessType.READ) + item = Item().load(itemId, user=user, level=AccessType.WRITE) + return Annotation().createAnnotation( + item, user, annotation['annotation'])
+ + +
+[docs] + @describeRoute( + Description('Update an annotation or move it to a different item.') + .param('id', 'The ID of the annotation.', paramType='path') + .param('itemId', 'Pass this to move the annotation to a new item.', + required=False) + .param('body', 'A JSON object containing the annotation. If the ' + '"annotation":"elements" property is not set, the elements ' + 'will not be modified.', + paramType='body', required=False) + .errorResponse('Write access was denied for the item.', 403) + .errorResponse('Invalid JSON passed in request body.') + .errorResponse("Validation Error: JSON doesn't follow schema."), + ) + @access.user(scope=TokenScope.DATA_WRITE) + @loadmodel(model='annotation', plugin='large_image', level=AccessType.WRITE) + @filtermodel(model='annotation', plugin='large_image') + def updateAnnotation(self, annotation, params): + # Set the response time limit to a very long value + setResponseTimeLimit(86400) + user = self.getCurrentUser() + item = Item().load(annotation.get('itemId'), force=True) + if item is not None: + Item().hasAccessFlags( + item, user, constants.ANNOTATION_ACCESS_FLAG) or Item().requireAccess( + item, user=user, level=AccessType.WRITE) + # If we have a content length, then we have replacement JSON. If + # elements are not included, don't replace them + returnElements = True + if cherrypy.request.body.length: + oldElements = annotation.get('annotation', {}).get('elements') + annotation['annotation'] = self.getBodyJson() + if 'elements' not in annotation['annotation'] and oldElements: + annotation['annotation']['elements'] = oldElements + returnElements = False + if params.get('itemId'): + newitem = Item().load(params['itemId'], force=True) + Item().hasAccessFlags( + newitem, user, constants.ANNOTATION_ACCESS_FLAG) or Item().requireAccess( + newitem, user=user, level=AccessType.WRITE) + annotation['itemId'] = newitem['_id'] + try: + annotation = Annotation().updateAnnotation(annotation, updateUser=user) + except ValidationException as exc: + logger.exception('Failed to validate annotation') + raise RestException( + "Validation Error: JSON doesn't follow schema (%r)." % ( + exc.args, )) + if not returnElements and 'elements' in annotation['annotation']: + del annotation['annotation']['elements'] + return annotation
+ + +
+[docs] + @describeRoute( + Description('Delete an annotation.') + .param('id', 'The ID of the annotation.', paramType='path') + .errorResponse('ID was invalid.') + .errorResponse('Write access was denied for the annotation.', 403), + ) + @access.user(scope=TokenScope.DATA_WRITE) + # Load with a limit of 1 so that we don't bother getting most annotations + @loadmodel(model='annotation', plugin='large_image', getElements=False, level=AccessType.WRITE) + def deleteAnnotation(self, annotation, params): + # Ensure that we have write access to the parent item + item = Item().load(annotation.get('itemId'), force=True) + if item is not None: + user = self.getCurrentUser() + Item().hasAccessFlags( + item, user, constants.ANNOTATION_ACCESS_FLAG) or Item().requireAccess( + item, user, level=AccessType.WRITE) + setResponseTimeLimit(86400) + Annotation().remove(annotation)
+ + +
+[docs] + @describeRoute( + Description('Search for annotated images.') + .notes( + 'By default, this endpoint will return a list of recently annotated images. ' + 'This list can be further filtered by passing the creatorId and/or imageName ' + 'parameters. The creatorId parameter will limit results to annotations ' + 'created by the given user. The imageName parameter will only include ' + 'images whose name (or a token in the name) begins with the given string.') + .param('creatorId', 'Limit to annotations created by this user', required=False) + .param('imageName', 'Filter results by image name (case-insensitive)', required=False) + .pagingParams(defaultSort='updated', defaultSortDir=-1) + .errorResponse(), + ) + @access.public(scope=TokenScope.DATA_READ) + def findAnnotatedImages(self, params): + limit, offset, sort = self.getPagingParameters( + params, 'updated', SortDir.DESCENDING) + user = self.getCurrentUser() + + creator = None + if 'creatorId' in params: + creator = User().load(params.get('creatorId'), force=True) + + return Annotation().findAnnotatedImages( + creator=creator, imageNameFilter=params.get('imageName'), + user=user, level=AccessType.READ, + offset=offset, limit=limit, sort=sort)
+ + +
+[docs] + @describeRoute( + Description('Get the access control list for an annotation.') + .param('id', 'The ID of the annotation.', paramType='path') + .errorResponse('ID was invalid.') + .errorResponse('Admin access was denied for the annotation.', 403), + ) + @access.user(scope=TokenScope.DATA_OWN) + @loadmodel(model='annotation', plugin='large_image', getElements=False, level=AccessType.ADMIN) + def getAnnotationAccess(self, annotation, params): + return Annotation().getFullAccessList(annotation)
+ + +
+[docs] + @describeRoute( + Description('Update the access control list for an annotation.') + .param('id', 'The ID of the annotation.', paramType='path') + .param('access', 'The JSON-encoded access control list.') + .param('public', 'Whether the annotation should be publicly visible.', + dataType='boolean', required=False) + .errorResponse('ID was invalid.') + .errorResponse('Admin access was denied for the annotation.', 403), + ) + @access.user(scope=TokenScope.DATA_OWN) + @loadmodel(model='annotation', plugin='large_image', getElements=False, level=AccessType.ADMIN) + @filtermodel(model=Annotation, addFields={'access'}) + def updateAnnotationAccess(self, annotation, params): + access = json.loads(params['access']) + public = self.boolParam('public', params, False) + annotation = Annotation().setPublic(annotation, public) + annotation = Annotation().setAccessList( + annotation, access, save=False, user=self.getCurrentUser()) + Annotation().update({'_id': annotation['_id']}, {'$set': { + key: annotation[key] for key in ('access', 'public', 'publicFlags') + if key in annotation + }}) + return annotation
+ + +
+[docs] + @autoDescribeRoute( + Description("Get a list of an annotation's history.") + .param('id', 'The ID of the annotation.', paramType='path') + .pagingParams(defaultSort='_version', defaultLimit=0, + defaultSortDir=SortDir.DESCENDING) + .errorResponse('Read access was denied for the annotation.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + def getAnnotationHistoryList(self, id, limit, offset, sort): + return list(Annotation().versionList(id, self.getCurrentUser(), limit, offset, sort))
+ + +
+[docs] + @autoDescribeRoute( + Description("Get a specific version of an annotation's history.") + .param('id', 'The ID of the annotation.', paramType='path') + .param('version', 'The version of the annotation.', paramType='path', + dataType='integer') + .errorResponse('Annotation history version not found.') + .errorResponse('Read access was denied for the annotation.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + def getAnnotationHistory(self, id, version): + result = Annotation().getVersion(id, version, self.getCurrentUser()) + if result is None: + msg = 'Annotation history version not found.' + raise RestException(msg) + return result
+ + +
+[docs] + @autoDescribeRoute( + Description('Revert an annotation to a specific version.') + .notes('This can be used to undelete an annotation by reverting to ' + 'the most recent version.') + .param('id', 'The ID of the annotation.', paramType='path') + .param('version', 'The version of the annotation. If not specified, ' + 'if the annotation was deleted this undeletes it. If it was ' + 'not deleted, this reverts to the previous version.', + required=False, dataType='integer') + .errorResponse('Annotation history version not found.') + .errorResponse('Read access was denied for the annotation.', 403), + ) + @access.public(scope=TokenScope.DATA_WRITE) + def revertAnnotationHistory(self, id, version): + setResponseTimeLimit(86400) + annotation = Annotation().revertVersion(id, version, self.getCurrentUser()) + if not annotation: + msg = 'Annotation history version not found.' + raise RestException(msg) + # Don't return the elements -- it can be too verbose + if 'elements' in annotation['annotation']: + del annotation['annotation']['elements'] + return annotation
+ + +
+[docs] + @autoDescribeRoute( + Description('Get all annotations for an item.') + .notes('This returns a list of annotation model records.') + .modelParam('id', model=Item, level=AccessType.READ) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + def getItemAnnotations(self, item): + user = self.getCurrentUser() + query = {'_active': {'$ne': False}, 'itemId': item['_id']} + + def generateResult(): + yield b'[' + first = True + for annotation in Annotation().find(query, limit=0, sort=[('_id', 1)]): + if not first: + yield b',\n' + try: + annotation = Annotation().load( + annotation['_id'], user=user, level=AccessType.READ, getElements=False) + annotationGenerator = self._getAnnotation(annotation, {})() + except AccessException: + continue + yield from annotationGenerator + first = False + yield b']' + + setResponseHeader('Content-Type', 'application/json') + return generateResult
+ + +
+[docs] + @autoDescribeRoute( + Description('Create multiple annotations on an item.') + .modelParam('id', model=Item, level=AccessType.WRITE) + # Use param instead of jsonParam; it lets us use a faster non-core json + # library + .param('annotations', 'A JSON list of annotation model records or ' + 'annotations. If these are complete models, the value of ' + 'the "annotation" key is used and the other information is ' + 'ignored (such as original creator ID).', paramType='body') + .errorResponse('ID was invalid.') + .errorResponse('Write access was denied for the item.', 403) + .errorResponse('Invalid JSON passed in request body.') + .errorResponse("Validation Error: JSON doesn't follow schema."), + ) + @access.user(scope=TokenScope.DATA_WRITE) + def createItemAnnotations(self, item, annotations): + user = self.getCurrentUser() + if hasattr(annotations, 'read'): + startTime = time.time() + annotations = annotations.read().decode('utf8') + annotations = orjson.loads(annotations) + if time.time() - startTime > 10: + logger.info('Decoded json in %5.3fs', time.time() - startTime) + if not isinstance(annotations, list): + annotations = [annotations] + for entry in annotations: + if not isinstance(entry, dict): + msg = 'Entries in the annotation list must be JSON objects.' + raise RestException(msg) + annotation = entry.get('annotation', entry) + try: + Annotation().createAnnotation(item, user, annotation) + except ValidationException as exc: + logger.exception('Failed to validate annotation') + raise RestException( + "Validation Error: JSON doesn't follow schema (%r)." % ( + exc.args, )) + return len(annotations)
+ + +
+[docs] + @autoDescribeRoute( + Description('Delete all annotations for an item.') + .notes('This deletes all annotation model records.') + .modelParam('id', model=Item, level=AccessType.WRITE) + .errorResponse('ID was invalid.') + .errorResponse('Write access was denied for the item.', 403), + ) + @access.user(scope=TokenScope.DATA_WRITE) + def deleteItemAnnotations(self, item): + setResponseTimeLimit(86400) + user = self.getCurrentUser() + query = {'_active': {'$ne': False}, 'itemId': item['_id']} + + count = 0 + for annotation in Annotation().find(query, limit=0, sort=[('_id', 1)]): + annot = Annotation().load(annotation['_id'], user=user, getElements=False) + if annot: + Annotation().remove(annot) + count += 1 + return count
+ + +
+[docs] + @autoDescribeRoute( + Description('Get a list of plottable data related to an item and its annotations.') + .modelParam('id', model=Item, level=AccessType.READ) + .param('adjacentItems', 'Whether to include adjacent item data.', + required=False, default=False, enum=['false', 'true', '__all__']) + .jsonParam('annotations', 'A JSON list of annotation IDs that should ' + 'be included. An entry of \\__all__ will include all ' + 'annotations.', paramType='formData', requireArray=True, + required=False) + .param('sources', 'An optional comma separated list that can contain ' + 'folder, item, annotation, annotationelement, datafile.', + required=False) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + def getItemPlottableElements(self, item, annotations, adjacentItems, sources=None): + user = self.getCurrentUser() + if adjacentItems != '__all__': + adjacentItems = str(adjacentItems).lower() == 'true' + sources = sources or None + data = utils.PlottableItemData( + user, item, annotations=annotations, adjacentItems=adjacentItems, + sources=sources) + return [col for col in data.columns if col.get('count')]
+ + +
+[docs] + @autoDescribeRoute( + Description('Get plottable data related to an item and its annotations.') + .modelParam('id', model=Item, level=AccessType.READ) + .param('adjacentItems', 'Whether to include adjacent item data.', + required=False, default=False, enum=['false', 'true', '__all__']) + .param('keys', 'A comma separated list of data keys to retrieve (not json).', + required=True) + .param('requiredKeys', 'A comma separated list of data keys that must ' + 'be non null in all response rows (not json).', required=False) + .jsonParam('annotations', 'A JSON list of annotation IDs that should ' + 'be included. An entry of \\__all__ will include all ' + 'annotations.', paramType='formData', requireArray=True, + required=False) + .param('sources', 'An optional comma separated list that can contain ' + 'folder, item, annotation, annotationelement, datafile.', + required=False) + .errorResponse('ID was invalid.') + .errorResponse('Read access was denied for the item.', 403), + ) + @access.public(cookie=True, scope=TokenScope.DATA_READ) + def getItemPlottableData( + self, item, keys, adjacentItems, annotations, requiredKeys, sources=None): + user = self.getCurrentUser() + if adjacentItems != '__all__': + adjacentItems = str(adjacentItems).lower() == 'true' + sources = sources or None + data = utils.PlottableItemData( + user, item, annotations=annotations, adjacentItems=adjacentItems, + sources=sources) + return data.data(keys, requiredKeys)
+ + +
+[docs] + def getFolderAnnotations(self, id, recurse, user, limit=False, offset=False, sort=False, + sortDir=False, count=False): + + accessPipeline = [ + {'$match': { + '$or': [ + {'access.users': + {'$elemMatch': { + 'id': user['_id'], + 'level': {'$gte': 2}, + }}}, + {'access.groups': + {'$elemMatch': { + 'id': {'$in': user['groups']}, + 'level': {'$gte': 2}, + }}}, + ], + }}, + ] if not user['admin'] else [] + recursivePipeline = [ + {'$match': {'_id': ObjectId(id)}}, + {'$graphLookup': { + 'from': 'folder', + 'startWith': ObjectId(id), + 'connectFromField': '_id', + 'connectToField': 'parentId', + 'as': '__children', + }}, + {'$lookup': { + 'from': 'folder', + 'localField': '_id', + 'foreignField': '_id', + 'as': '__self', + }}, + {'$project': {'__children': {'$concatArrays': [ + '$__self', '$__children', + ]}}}, + {'$unwind': {'path': '$__children'}}, + {'$replaceRoot': {'newRoot': '$__children'}}, + ] if recurse else [{'$match': {'_id': ObjectId(id)}}] + + # We are only finding anntoations that we can change the permissions + # on. If we wanted to expose annotations based on a permissions level, + # we need to add a folder access pipeline immediately after the + # recursivePipleine that for write and above would include the + # ANNOTATION_ACCSESS_FLAG + pipeline = recursivePipeline + [ + {'$lookup': { + 'from': 'item', + # We have to use a pipeline to use a projection to reduce the + # data volume, so instead of specifying localField and + # foreignField, we set the localField to a variable, then match + # it in a pipeline and project to exclude everything but id. + # 'localField': '_id', + # 'foreignField': 'folderId', + 'let': {'fid': '$_id'}, + 'pipeline': [ + {'$match': {'$expr': {'$eq': ['$$fid', '$folderId']}}}, + {'$project': {'_id': 1}}, + ], + 'as': '__items', + }}, + {'$lookup': { + 'from': 'annotation', + 'localField': '__items._id', + 'foreignField': 'itemId', + 'as': '__annotations', + }}, + {'$unwind': '$__annotations'}, + {'$replaceRoot': {'newRoot': '$__annotations'}}, + {'$match': {'_active': {'$ne': False}}}, + ] + accessPipeline + + if count: + pipeline += [{'$count': 'count'}] + else: + pipeline = pipeline + [{'$sort': {sort: sortDir}}] if sort else pipeline + pipeline = pipeline + [{'$skip': offset}] if offset else pipeline + pipeline = pipeline + [{'$limit': limit}] if limit else pipeline + return Folder().collection.aggregate(pipeline)
+ + +
+[docs] + @autoDescribeRoute( + Description('Check if the user owns any annotations for the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('recurse', 'Whether or not to recursively check ' + 'subfolders for annotations', required=False, default=True, dataType='boolean') + .errorResponse(), + ) + @access.public(scope=TokenScope.DATA_READ) + def existFolderAnnotations(self, id, recurse): + user = self.getCurrentUser() + if not user: + return [] + annotations = self.getFolderAnnotations(id, recurse, user, 1) + try: + next(annotations) + return True + except StopIteration: + return False
+ + +
+[docs] + @autoDescribeRoute( + Description('Get the user-owned annotations from the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('recurse', 'Whether or not to retrieve all ' + 'annotations from subfolders', required=False, default=False, dataType='boolean') + .pagingParams(defaultSort='created', defaultSortDir=-1) + .errorResponse(), + ) + @access.public(scope=TokenScope.DATA_READ) + def returnFolderAnnotations(self, id, recurse, limit, offset, sort): + user = self.getCurrentUser() + if not user: + return [] + annotations = self.getFolderAnnotations(id, recurse, user, limit, offset, + sort[0][0], sort[0][1]) + + def count(): + try: + return next(self.getFolderAnnotations(id, recurse, self.getCurrentUser(), + count=True))['count'] + except StopIteration: + # If there are no values to iterate over, the count is 0 and should be returned + return 0 + + annotations.count = count + return annotations
+ + +
+[docs] + @autoDescribeRoute( + Description('Check if the user can create annotations in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .errorResponse('ID was invalid.'), + ) + @access.user(scope=TokenScope.DATA_READ) + @loadmodel(model='folder', level=AccessType.READ) + def canCreateFolderAnnotations(self, folder): + user = self.getCurrentUser() + return Folder().hasAccess(folder, user, AccessType.WRITE) or Folder().hasAccessFlags( + folder, user, constants.ANNOTATION_ACCESS_FLAG)
+ + +
+[docs] + @autoDescribeRoute( + Description('Set the access for all the user-owned annotations from the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('access', 'The JSON-encoded access control list.') + .param('public', 'Whether the annotation should be publicly visible.', + dataType='boolean', required=False) + .param('recurse', 'Whether or not to retrieve all ' + 'annotations from subfolders', required=False, default=False, dataType='boolean') + .errorResponse('ID was invalid.'), + ) + @access.user(scope=TokenScope.DATA_OWN) + def setFolderAnnotationAccess(self, id, params): + setResponseTimeLimit(86400) + user = self.getCurrentUser() + if not user: + return [] + access = json.loads(params['access']) + public = self.boolParam('public', params, False) + count = 0 + for annotation in self.getFolderAnnotations(id, params['recurse'], user): + annot = Annotation().load(annotation['_id'], user=user, getElements=False) + annot = Annotation().setPublic(annot, public) + annot = Annotation().setAccessList( + annot, access, user=user) + Annotation().update({'_id': annot['_id']}, {'$set': { + key: annot[key] for key in ('access', 'public', 'publicFlags') + if key in annot + }}) + count += 1 + + return {'updated': count}
+ + +
+[docs] + @autoDescribeRoute( + Description('Delete all user-owned annotations from the items in a folder') + .param('id', 'The ID of the folder', required=True, paramType='path') + .param('recurse', 'Whether or not to retrieve all ' + 'annotations from subfolders', required=False, default=False, dataType='boolean') + .errorResponse('ID was invalid.'), + ) + @access.user(scope=TokenScope.DATA_WRITE) + def deleteFolderAnnotations(self, id, params): + setResponseTimeLimit(86400) + user = self.getCurrentUser() + if not user: + return [] + count = 0 + for annotation in self.getFolderAnnotations(id, params['recurse'], user): + annot = Annotation().load(annotation['_id'], user=user, getElements=False) + Annotation().remove(annot) + count += 1 + + return {'deleted': count}
+ + +
+[docs] + @autoDescribeRoute( + Description('Report on old annotations.') + .param('age', 'The minimum age in days.', required=False, + dataType='int', default=30) + .param('versions', 'Keep at least this many history entries for each ' + 'annotation.', required=False, dataType='int', default=10) + .errorResponse(), + ) + @access.admin(scope=TokenScope.DATA_READ) + def getOldAnnotations(self, age, versions): + setResponseTimeLimit(86400) + return Annotation().removeOldAnnotations(False, age, versions)
+ + +
+[docs] + @autoDescribeRoute( + Description('Delete old annotations.') + .param('age', 'The minimum age in days.', required=False, + dataType='int', default=30) + .param('versions', 'Keep at least this many history entries for each ' + 'annotation.', required=False, dataType='int', default=10) + .errorResponse(), + ) + @access.admin(scope=TokenScope.DATA_WRITE) + def deleteOldAnnotations(self, age, versions): + setResponseTimeLimit(86400) + return Annotation().removeOldAnnotations(True, age, versions)
+ + +
+[docs] + @access.public(scope=TokenScope.DATA_READ) + @autoDescribeRoute( + Description('Get annotation counts for a list of items.') + .param('items', 'A comma-separated list of item ids.') + .errorResponse(), + ) + def getItemListAnnotationCounts(self, items): + user = self.getCurrentUser() + results = {} + for itemId in items.split(','): + item = Item().load(itemId, level=AccessType.READ, user=user) + annotations = Annotation().findWithPermissions( + {'_active': {'$ne': False}, 'itemId': item['_id']}, + user=self.getCurrentUser(), level=AccessType.READ, limit=-1) + results[itemId] = annotations.count() + if Annotationelement().findOne({'element.girderId': itemId}): + if 'referenced' not in results: + results['referenced'] = {} + results['referenced'][itemId] = True + return results
+ + +
+[docs] + @access.user(scope=TokenScope.DATA_WRITE) + @filtermodel(model='annotation', plugin='large_image') + @autoDescribeRoute( + Description('Set metadata (annotation.attributes) fields on an annotation.') + .responseClass('Annotation') + .notes('Set metadata fields to null in order to delete them.') + .param('id', 'The ID of the annotation.', paramType='path') + .jsonParam('metadata', 'A JSON object containing the metadata keys to add', + paramType='body', requireObject=True) + .param('allowNull', 'Whether "null" is allowed as a metadata value.', required=False, + dataType='boolean', default=False) + .errorResponse(('ID was invalid.', + 'Invalid JSON passed in request body.', + 'Metadata key name was invalid.')) + .errorResponse('Write access was denied for the annotation.', 403), + ) + @loadmodel(model='annotation', plugin='large_image', getElements=False, level=AccessType.WRITE) + def setMetadata(self, annotation, metadata, allowNull): + return Annotation().setMetadata(annotation, metadata, allowNull=allowNull)
+ + +
+[docs] + @access.user(scope=TokenScope.DATA_WRITE) + @filtermodel(model='annotation', plugin='large_image') + @autoDescribeRoute( + Description('Delete metadata (annotation.attributes) fields on an annotation.') + .responseClass('Item') + .param('id', 'The ID of the annotation.', paramType='path') + .jsonParam( + 'fields', 'A JSON list containing the metadata fields to delete', + paramType='body', schema={ + 'type': 'array', + 'items': { + 'type': 'string', + }, + }, + ) + .errorResponse(('ID was invalid.', + 'Invalid JSON passed in request body.', + 'Metadata key name was invalid.')) + .errorResponse('Write access was denied for the annotation.', 403), + ) + @loadmodel(model='annotation', plugin='large_image', getElements=False, level=AccessType.WRITE) + def deleteMetadata(self, annotation, fields): + return Annotation().deleteMetadata(annotation, fields)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/girder_large_image_annotation/utils.html b/_modules/girder_large_image_annotation/utils.html new file mode 100644 index 000000000..55ee9fff0 --- /dev/null +++ b/_modules/girder_large_image_annotation/utils.html @@ -0,0 +1,1436 @@ + + + + + + girder_large_image_annotation.utils — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for girder_large_image_annotation.utils

+import functools
+import json
+import math
+import os
+import re
+import threading
+
+from bson.objectid import ObjectId
+
+from girder import logger
+from girder.constants import AccessType, SortDir
+from girder.models.file import File
+from girder.models.folder import Folder
+from girder.models.item import Item
+
+dataFileExtReaders = {
+    '.csv': 'read_csv',
+    'text/csv': 'read_csv',
+    '.xls': 'read_excel',
+    '.xlsx': 'read_excel',
+    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'read_excel',
+    'application/vnd.ms-excel ': 'read_excel',
+    'application/msexcel': 'read_excel',
+    'application/x-msexcel': 'read_excel',
+    'application/x-ms-excel': 'read_excel',
+    'application/x-excel': 'read_excel',
+    'application/x-dos_ms_excel': 'read_excel',
+    'application/xls': 'read_excel',
+    'application/x-xls': 'read_excel',
+}
+scanDatafileRecords = 50
+
+
+@functools.lru_cache(maxsize=100)
+def _dfFromFile(fileid, full=False):
+    import pandas as pd
+
+    file = File().load(fileid, force=True)
+    ext = os.path.splitext(file['name'])[1]
+    reader = dataFileExtReaders.get(
+        ext, dataFileExtReaders.get(file.get('mimeType'), None))
+    if reader == 'read_excel':
+        df = getattr(pd, reader)(File().open(file), sheet_name=None)
+    else:
+        df = {'entry': getattr(pd, reader)(File().open(file))}
+    df = {
+        k: sheet.iloc[:None if full else scanDatafileRecords].to_dict('records')
+        for k, sheet in df.items()}
+    logger.info(f'Read {len(df)} x {len(next(iter(df.values())))} values from '
+                f'{file["name"]} {file["size"]}')
+    if len(df) == 1:
+        df = next(iter(df.values()))
+    return df
+
+
+
+[docs] +class AnnotationGeoJSON: + """ + Generate GeoJSON for an annotation via an iterator. + """ + + def __init__(self, annotationId, asFeatures=False, mustConvert=False): + """ + Return an itertor for converting an annotation into geojson. + + :param annotatioId: the id of the annotation. No permissions checks + are performed. + :param asFeatures: if False, return a geojson string. If True, return + the features of the geojson. This can be wrapped in + `{'type': 'FeatureCollection', 'features: [...output...]}` + to make it a full geojson object. + :param mustConvert: if True, raise an exception if any annotation + elements cannot be converted. Otherwise, skip those elements. + """ + from ..models.annotation import Annotation + from ..models.annotationelement import Annotationelement + + self._id = annotationId + self.annotation = Annotation().load(id=self._id, force=True, getElements=False) + self.elemIterator = Annotationelement().yieldElements(self.annotation) + self.stage = 'header' + self.first = self.annotation['annotation'] + self.asFeatures = asFeatures + self.mustConvert = mustConvert + + def __iter__(self): + from ..models.annotationelement import Annotationelement + + self.elemIterator = Annotationelement().yieldElements(self.annotation) + self.stage = 'header' + return self + + def __next__(self): + if self.stage == 'header': + self.stage = 'firstelement' + if not self.asFeatures: + return '{"type":"FeatureCollection","features":[' + if self.stage == 'done': + raise StopIteration + try: + while True: + element = next(self.elemIterator) + result = self.elementToGeoJSON(element) + if result is not None: + break + if self.mustConvert: + msg = f'Element of type {element["type"]} cannot be represented as geojson' + raise Exception(msg) + prefix = '' + if self.stage == 'firstelement': + result['properties']['annotation'] = self.first + self.stage = 'elements' + else: + prefix = ',' + if not self.asFeatures: + return prefix + json.dumps(result, separators=(',', ':')) + return result + except StopIteration: + self.stage = 'done' + if not self.asFeatures: + return ']}' + raise + +
+[docs] + def rotate(self, r, cx, cy, x, y, z): + if not r: + return [x, y, z] + cosr = math.cos(r) + sinr = math.sin(r) + x -= cx + y -= cy + return [x * cosr - y * sinr + cx, x * sinr + y * sinr + cy, z]
+ + +
+[docs] + def circleType(self, element, geom, prop): + x, y, z = element['center'] + r = element['radius'] + geom['type'] = 'Polygon' + geom['coordinates'] = [[ + [x - r, y - r, z], + [x + r, y - r, z], + [x + r, y + r, z], + [x - r, y + r, z], + [x - r, y - r, z], + ]]
+ + +
+[docs] + def ellipseType(self, element, geom, prop): + return self.rectangleType(element, geom, prop)
+ + +
+[docs] + def pointType(self, element, geom, prop): + geom['type'] = 'Point' + geom['coordinates'] = element['center']
+ + +
+[docs] + def polylineType(self, element, geom, prop): + if element['closed']: + geom['type'] = 'Polygon' + geom['coordinates'] = [element['points'][:]] + geom['coordinates'][0].append(geom['coordinates'][0][0]) + if element.get('holes'): + for hole in element['holes']: + hole = hole[:] + hole.append(hole[0]) + geom['coordinates'].append(hole) + else: + geom['type'] = 'LineString' + geom['coordinates'] = element['points']
+ + +
+[docs] + def rectangleType(self, element, geom, prop): + x, y, z = element['center'] + width = element['width'] + height = element['height'] + rotation = element.get('rotation', 0) + left = x - width / 2 + right = x + width / 2 + top = y - height / 2 + bottom = y + height / 2 + + geom['type'] = 'Polygon' + geom['coordinates'] = [[ + self.rotate(rotation, x, y, left, top, z), + self.rotate(rotation, x, y, right, top, z), + self.rotate(rotation, x, y, right, bottom, z), + self.rotate(rotation, x, y, left, bottom, z), + self.rotate(rotation, x, y, left, top, z), + ]]
+ + + # not represented + # heatmap, griddata, image, pixelmap, arrow, rectanglegrid + # heatmap could be MultiPoint, griddata could be rectangle with lots of + # properties, image and pixelmap could be rectangle with the image id as a + # property, arrow and rectangelgrid aren't really supported + +
+[docs] + def elementToGeoJSON(self, element): + elemType = element.get('type', '') + funcName = elemType + 'Type' + if not hasattr(self, funcName): + return None + result = { + 'type': 'Feature', + 'geometry': {}, + 'properties': { + k: v if k != 'id' else str(v) + for k, v in element.items() if k in { + 'id', 'label', 'group', 'user', 'lineColor', 'lineWidth', + 'fillColor', 'radius', 'width', 'height', 'rotation', + 'normal', + } + }, + } + getattr(self, funcName)(element, result['geometry'], result['properties']) + if result['geometry']['type'].lower() != element['type']: + result['properties']['type'] = element['type'] + return result
+ + + @property + def geojson(self): + return ''.join(self)
+ + + +
+[docs] +class GeoJSONAnnotation: + def __init__(self, geojson): + if not isinstance(geojson, (dict, list, tuple)): + geojson = json.loads(geojson) + self._elements = [] + self._annotation = {'elements': self._elements} + self._parseFeature(geojson) + + def _parseFeature(self, geoelem): + if isinstance(geoelem, (list, tuple)): + for entry in geoelem: + self._parseFeature(entry) + if not isinstance(geoelem, dict) or 'type' not in geoelem: + return + if geoelem['type'] == 'FeatureCollection': + return self._parseFeature(geoelem.get('features', [])) + if geoelem['type'] == 'GeometryCollection' and isinstance(geoelem.get('geometries'), list): + for entry in geoelem['geometry']: + self._parseFeature({'type': 'Feature', 'geometry': entry}) + return + if geoelem['type'] in {'Point', 'LineString', 'Polygon', 'MultiPoint', + 'MultiLineString', 'MultiPolygon'}: + geoelem = {'type': 'Feature', 'geometry': geoelem} + element = {k: v for k, v in geoelem.get('properties', {}).items() if k in { + 'id', 'label', 'group', 'user', 'lineColor', 'lineWidth', + 'fillColor', 'radius', 'width', 'height', 'rotation', + 'normal', + }} + if 'annotation' in geoelem.get('properties', {}): + self._annotation.update(geoelem['properties']['annotation']) + self._annotation['elements'] = self._elements + elemtype = geoelem.get('properties', {}).get('type', '') or geoelem['geometry']['type'] + func = getattr(self, elemtype.lower() + 'Type', None) + if func is not None: + result = func(geoelem['geometry'], element) + if isinstance(result, list): + self._elements.extend(result) + else: + self._elements.append(result) + +
+[docs] + def circleType(self, elem, result): + cx = sum(e[0] for e in elem['coordinates'][0][:4]) / 4 + cy = sum(e[1] for e in elem['coordinates'][0][:4]) / 4 + try: + cz = elem['coordinates'][0][0][2] + except Exception: + cz = 0 + radius = (max(e[0] for e in elem['coordinates'][0][:4]) - + min(e[0] for e in elem['coordinates'][0][:4])) / 2 + result['type'] = 'circle' + result['radius'] = radius + result['center'] = [cx, cy, cz] + return result
+ + +
+[docs] + def ellipseType(self, elem, result): + result = self.rectangleType(elem, result) + result['type'] = 'ellipse' + return result
+ + +
+[docs] + def rectangleType(self, elem, result): + coor = elem['coordinates'][0] + cx = sum(e[0] for e in coor[:4]) / 4 + cy = sum(e[1] for e in coor[:4]) / 4 + try: + cz = elem['coordinates'][0][0][2] + except Exception: + cz = 0 + width = ((coor[0][0] - coor[1][0]) ** 2 + (coor[0][1] - coor[1][1]) ** 2) ** 0.5 + height = ((coor[1][0] - coor[2][0]) ** 2 + (coor[1][1] - coor[2][1]) ** 2) ** 0.5 + rotation = math.atan2(coor[1][1] - coor[0][1], coor[1][0] - coor[0][0]) + result['center'] = [cx, cy, cz] + result['width'] = width + result['height'] = height + result['rotation'] = rotation + result['type'] = 'rectangle' + return result
+ + +
+[docs] + def pointType(self, elem, result): + result['center'] = (elem['coordinates'] + [0, 0, 0])[:3] + result['type'] = 'point' + return result
+ + +
+[docs] + def multipointType(self, elem, result): + results = [] + result['type'] = 'point' + for entry in elem['coordinates']: + subresult = result.copy() + subresult['center'] = (entry + [0, 0, 0])[:3] + results.append(subresult) + return results
+ + +
+[docs] + def polylineType(self, elem, result): + if elem.get('type') == 'LineString': + return self.linestringType(elem, result) + return self.polygonType(elem, result)
+ + +
+[docs] + def polygonType(self, elem, result): + result['points'] = [(pt + [0])[:3] for pt in elem['coordinates'][0][:-1]] + if len(elem['coordinates']) > 1: + result['holes'] = [ + [(pt + [0])[:3] for pt in loop[:-1]] + for loop in elem['coordinates'][1:] + ] + result['closed'] = True + result['type'] = 'polyline' + return result
+ + +
+[docs] + def multipolygonType(self, elem, result): + results = [] + result['closed'] = True + result['type'] = 'polyline' + for entry in elem['coordinates']: + subresult = result.copy() + subresult['points'] = [(pt + [0])[:3] for pt in entry[0][:-1]] + if len(entry) > 1: + subresult['holes'] = [ + [(pt + [0])[:3] for pt in loop[:-1]] + for loop in entry[1:] + ] + results.append(subresult) + return results
+ + +
+[docs] + def linestringType(self, elem, result): + result['points'] = [(pt + [0])[:3] for pt in elem['coordinates']] + result['closed'] = False + result['type'] = 'polyline' + return result
+ + +
+[docs] + def multilinestringType(self, elem, result): + results = [] + result['closed'] = False + result['type'] = 'polyline' + for entry in elem['coordinates']: + subresult = result.copy() + subresult['points'] = [(pt + [0])[:3] for pt in entry] + results.append(subresult) + return results
+ + +
+[docs] + def annotationToJSON(self): + return json.dumps(self._annotation)
+ + + @property + def annotation(self): + return self._annotation + + @property + def elements(self): + return self._elements + + @property + def elementCount(self): + return len(self._elements)
+ + + +
+[docs] +def isGeoJSON(annotation): + """ + Check if a list or dictionary appears to contain a GeoJSON record. + + :param annotation: a list or dictionary. + :returns: True if this appears to be GeoJSON + """ + if isinstance(annotation, list): + if len(annotation) < 1: + return False + annotation = annotation[0] + if not isinstance(annotation, dict) or 'type' not in annotation: + return False + return annotation['type'] in { + 'Feature', 'FeatureCollection', 'GeometryCollection', 'Point', + 'LineString', 'Polygon', 'MultiPoint', 'MultiLineString', + 'MultiPolygon'}
+ + + +
+[docs] +class PlottableItemData: + maxItems = 1000 + maxAnnotationElements = 5000 + maxDistinct = 20 + allowedTypes = (str, bool, int, float) + + def __init__(self, user, item, annotations=None, adjacentItems=False, sources=None): + """ + Get plottable data associated with an item. + + :param user: authenticating user. + :param item: the item record. + :param annotations: None, a list of annotation ids, or __all__. If + adjacent items are included, the most recent annotation with the + same name will also be included. + :param adjacentItems: if True, include data from other items in the + same folder. If __all__, include data from other items even if the + data is not present in the current item. + :param sources: None for all, or a string with a comma-separated list + or a list of strings; when a list, the options are folder, item, + annotation, datafile. + """ + self.user = user + self._columns = None + self._datacolumns = None + self._data = None + if sources and not isinstance(sources, (list, tuple)): + sources = sources.split(',') + self._sources = tuple(sources) if sources else None + if self._sources and 'annotation' not in self._sources: + annotations = None + self._fullScan = adjacentItems == '__all__' + self._findItems(item, adjacentItems) + self._findAnnotations(annotations) + self._findDataFiles() + self._dataLock = threading.RLock() + + def _findItems(self, item, adjacentItems=False): + """ + Find all the large images in the folder. This only retrieves the first + self.maxItems entries. If there are at least this many items, a query + is stored in self._moreItems. The items are listed in self.items. + + :param item: the item to use as the base. If adjacentItems is false, + this is the entire self.items data set. + :param adjacentItems: if truthy, find adjacent items. + """ + self._columns = None + self.item = item + self.folder = Folder().load(id=item['folderId'], user=self.user, level=AccessType.READ) + self.items = [item] + if adjacentItems: + query = { + 'filters': { + '_id': {'$ne': item['_id']}, + }, + 'sort': [('_id', SortDir.ASCENDING)], + } + if 'largeImage' in item: + query['filters']['largeImage.fileId'] = {'$exists': True} + self.items.extend(list(Folder().childItems( + self.folder, limit=self.maxItems - 1, **query))) + self._moreItems = query if len(self.items) == self.maxItems else None + # TODO: find csv/xlsx/dataframe items in the folder + + def _findAnnotations(self, annotations): + """ + Find annotations based on a list of annotations ids. For the current + item, these are just the listed annotations. For adjacent items, + annotations with the same names are located. A maximum of maxItems + are examined, so if the number of items in the folder exceeds this, + some annotations will not be located. Results are stored in + self.annotations, which is a list with one entry per item. Each entry + is a list of annotations (without elements) or None if there is no + matching annotation for that item. + + :param annotations: a list of annotation id strings or comma-separated + string of annotation ids. + """ + from ..models.annotation import Annotation + + self._columns = None + if isinstance(annotations, str): + annotations = annotations.split(',') + self.annotations = None + if annotations and len(annotations): + self.annotations = [] + query = {'_active': {'$ne': False}, 'itemId': self.item['_id']} + if annotations[0] != '__all__': + query['_id'] = {'$in': [ObjectId(annotId) for annotId in annotations]} + self.annotations.append(list(Annotation().find( + query, limit=0, sort=[('_version', -1)]))) + if not len(self.annotations[0]): + self.annotations = None + # Find adjacent annotations + if annotations and len(self.items) > 1: + names = {} + for idx, annot in enumerate(self.annotations[0]): + if annot['annotation']['name'] not in names: + names[annot['annotation']['name']] = idx + for adjitem in self.items[1:]: + query = {'_active': {'$ne': False}, 'itemId': adjitem['_id']} + annotList = [None] * len(self.annotations[0]) + for annot in Annotation().find(query, limit=0, sort=[('_version', -1)]): + if annot['annotation']['name'] in names and annotList[ + names[annot['annotation']['name']]] is None: + annotList[names[annot['annotation']['name']]] = annot + self.annotations.append(annotList) + + def _findDataFiles(self): # noqa + """ + Find data files inside the current item. For adjacent items, the data + file must have the same name or, if the found file is prefixed with + the item name excluding the extension, then the adjancant file should + be similarly prefixed. Data files must have a known suffix or a known + mimetype that can be read by pandas (and pandas must be installed). + """ + self._itemfilelist = [[]] * len(self.items) + try: + import pandas as pd # noqa + except Exception: + return + if self._sources and 'filedata' not in self._sources: + return + names0 = {} + for iidx, item in enumerate(self.items): + if iidx: + self._itemfilelist[iidx] = [None] * len(self._itemfilelist[0]) + names = {} + for file in Item().childFiles(item): + try: + if (file['_id'] == self.item['largeImage']['fileId'] or + file['_id'] == self.item['largeImage'].get('originalId')): + continue + except Exception: + continue + ext = os.path.splitext(file['name'])[1] + if (ext not in dataFileExtReaders and + file.get('mimeType') not in dataFileExtReaders): + continue + if file['name'].startswith(item['name'].rsplit('.')[0]): + base, name = True, file['name'][len(item['name'].rsplit('.')[0]):] + else: + base, name = False, file['name'] + if (base, name) in names: + continue + if iidx and (base, name) not in names0: + continue + names[(base, name)] = len(names) + if not iidx: + self._itemfilelist[0].append(file) + else: + self._itemfilelist[iidx][names0[(base, name)]] = file + if not iidx: + names0 = names + + # Common column keys and titles + commonColumns = { + 'item.id': 'Item ID', + 'item.name': 'Item Name', + 'item.description': 'Item Description', + 'annotation.id': 'Annotation ID', + 'annotation.name': 'Annotation Name', + 'annotation.description': 'Annotation Description', + 'annotationelement.id': 'Annotation Element ID', + 'annotationelement.group': 'Annotation Element Group', + 'annotationelement.label': 'Annotation Element Label', + 'annotationelement.type': 'Annotation Element Type', + 'bbox.x0': 'Bounding Box Low X', + 'bbox.y0': 'Bounding Box Low Y', + 'bbox.x1': 'Bounding Box High X', + 'bbox.y1': 'Bounding Box High Y', + } + +
+[docs] + def itemNameIDSelector(self, isName, selector): + """ + Given a data selector that returns something that is either an item id, + an item name, or an item name prefix, return the canonical item or + id string from the list of known items. + + :param isName: True to return the canonical name, False for the + canonical id. + :param selector: the selector to get the initial value. + :returns: a function that can be used as an overall selector. + """ + + def itemNameSelector(record, data, row): + value = selector(record, data, row) + for item in self.items: + if str(item['_id']) == value: + return item['name'] + if item['name'].lower().startswith(value.lower() + '.'): + return item['name'] + if item['name'].lower() == value.lower(): + return item['name'] + return value + + def itemIDSelector(record, data, row): + value = selector(record, data, row) + for item in self.items: + if str(item['_id']) == value: + return str(item['_id']) + if item['name'].lower().startswith(value.lower() + '.'): + return str(item['_id']) + if item['name'].lower() == value.lower(): + return str(item['_id']) + return value + + return itemNameSelector if isName else itemIDSelector
+ + +
+[docs] + def datafileAnnotationElementSelector(self, key, cols): + + def annotationElementSelector(record, data, row): + bbox = [col[1](record, data, row) for col in cols] + if key in self._datacolumns: + for row in self._datacolumns[key]: + if self._datacolumns[key][row] is not None: + for bidx, bkey in enumerate(['bbox.x0', 'bbox.y0', 'bbox.x1', 'bbox.y1']): + val = self._datacolumns[bkey].get(row) + if val is None or abs(val - bbox[bidx]) > 2: + break + else: + return self._datacolumns[key][row] + return None + + return annotationElementSelector
+ + +
+[docs] + @staticmethod + def keySelector(mode, key, key2=None): + """ + Given a pattern for getting data from a dictionary, return a selector + that gets that piece of data. + + :param mode: one of key, key0, keykey, keykey0, key0key, representing + key lookups in dictionaries or array indices. + :param key: the first key. + :param key2: the second key, if needed. + :returns: a pair of functions that can be used to select the value from + the record and data structure. This takes (record, data, row) and + returns a value. The record is the base record used, the data is + the base dictionary, and the row is the location in the index. The + second function takes (record, data) and returns either None or the + number of rows that are present. + """ + if mode == 'key0key': + + def key0keySelector(record, data, row): + return data[key][row][key2] + + def key0keyLength(record, data): + return len(data[key]) + + return key0keySelector, key0keyLength + if mode == 'keykey0': + + def keykey0Selector(record, data, row): + return data[key][key2][row] + + def keykey0Length(record, data): + return len(data[key][key2]) + + return keykey0Selector, keykey0Length + if mode == 'keykey': + + def keykeySelector(record, data, row): + return data[key][key2] + + return keykeySelector, None + if mode == 'key0': + + def key0Selector(record, data, row): + return data[key][row] + + def key0Length(record, data): + return len(data[key]) + + return key0Selector, key0Length + + def keySelector(record, data, row): + return data[key] + + return keySelector, None
+ + +
+[docs] + @staticmethod + def recordSelector(doctype): + """ + Given a document type, return a function that returns the main data + dictionary. + + :param doctype: one of folder, item, annotaiton, annotationelement. + :returns: a function that takes (record) and returns the data + dictionary, if any. + """ + if doctype == 'annotation': + + def annotationGetData(record): + return record.get('annotation', {}).get('attributes', {}) + + return annotationGetData + if doctype == 'annotationelement': + + def annotationelementGetData(record): + return record.get('user', {}) + + return annotationelementGetData + + if doctype == 'datafile': + + def datafileGetData(record): + return record + + return datafileGetData + + def getData(record): + return record.get('meta', {}) + + return getData
+ + + def _keysToColumns(self, columns, parts, doctype, getData, selector, length): + """ + Given a selector and appropriate access information, ensure that an + appropriate column or columns exist. + + :param columns: the column dictionary to possibly modify. + :param parts: a tuple of values used to construct a key. + :param doctype: the base document type. + :param getData: a function that, given the document record, returns the + data dictionary. + :param selector: a function that, given the document record, data + dictionary, and row, returns a value. + :param length: None or a function that, given the document record and + data dictionary, returns the number of rows. + """ + key = '.'.join(str(v) for v in parts).lower() + lastpart = parts[-1] if parts[-1] != '0' or len(parts) == 1 else parts[-2] + title = ' '.join(str(v) for v in parts[1:] if v != '0') + keymap = { + r'(?i)(item|image)_(id|name)$': 'item.name', + r'(?i)((low|min)(_|)x|^x1$)': 'bbox.x0', + r'(?i)((low|min)(_|)y|^y1$)': 'bbox.y0', + r'(?i)((high|max)(_|)x|^x2$)': 'bbox.x1', + r'(?i)((high|max)(_|)y|^y2$)': 'bbox.y1', + } + match = False + for k, v in keymap.items(): + if re.match(k, lastpart): + if lastpart != parts[1]: + doctype = f'{doctype}.{parts[1]}' + key = v + title = self.commonColumns[key] + if key == 'item.name': + self._ensureColumn( + columns, key, title, doctype, getData, + self.itemNameIDSelector(True, selector), length) + self._ensureColumn( + columns, 'item.id', self.commonColumns['item.id'], + doctype, getData, + self.itemNameIDSelector(False, selector), length) + return + match = True + break + added = self._ensureColumn( + columns, key, title, doctype, getData, selector, length) + if match and added and key.startswith('bbox'): + cols = [columns[bkey]['where'][doctype] for bkey in [ + 'bbox.x0', 'bbox.y0', 'bbox.x1', 'bbox.y1'] + if bkey in columns and doctype in columns[bkey]['where']] + if len(cols) == 4: + # If we load all of these from annotation elements, use all + # three keys: + # for akey in {'annotation.id', 'annotation.name', 'annotationelement.id'}: + for akey in {'annotationelement.id'}: + if self._datacolumns and akey in self._datacolumns: + self._requiredColumns.add(akey) + self._ensureColumn( + columns, akey, self.commonColumns[akey], doctype, + getData, self.datafileAnnotationElementSelector(akey, cols), + length) + + def _ensureColumn(self, columns, keyname, title, doctype, getData, selector, length): + """ + Ensure that column exists and the selectors are recorded for the + doctype. + + :param columns: the column dictionary to possibly modify. + :param keyname: the key to the column. + :param title: the title of the column. + :param doctype: the base document type. + :param getData: a function that, given the document record, returns the + data dictionary. + :param selector: a function that, given the document record, data + dictionary, and row, returns a value. + :param length: None or a function that, given the document record and + data dictionary, returns the number of rows. + :returns: True if the column where record was added. + """ + if keyname not in columns: + columns[keyname] = { + 'key': keyname, + 'title': title, + 'where': {}, + 'type': 'number', + 'max': None, + 'min': None, + 'distinct': set(), + 'count': 0, + } + if doctype not in columns[keyname]['where']: + columns[keyname]['where'][doctype] = (getData, selector, length) + return True + return False + + def _columnsFromData(self, columns, doctype, getData, record): # noqa + """ + Given a sample record, determine what columns could be read. + + :param columns: the column dictionary to possibly modify. + :param doctype: the base document type. + :param getData: a function that, given the document record, returns the + data dictionary. + :param record: a sample record. + """ + data = getData(record) + for key, value in data.items(): + try: + if isinstance(value, list): + if not len(value): + continue + if isinstance(value[0], dict): + for key2, value2 in value[0].items(): + try: + if isinstance(value2, (list, dict)): + continue + selector, length = self.keySelector('key0key', key, key2) + self._keysToColumns( + columns, ('data', key, '0', key2), + doctype, getData, selector, length) + except Exception: + continue + else: + selector, length = self.keySelector('key0', key) + self._keysToColumns( + columns, ('data', key, '0'), + doctype, getData, selector, length) + elif isinstance(value, dict): + for key2, value2 in value.items(): + try: + if isinstance(value2, list): + if not len(value2): + continue + selector, length = self.keySelector('keykey0', key, key2) + self._keysToColumns( + columns, ('data', key, key2, '0'), + doctype, getData, selector, length) + else: + selector, length = self.keySelector('keykey', key, key2) + self._keysToColumns( + columns, ('data', key, key2), + doctype, getData, selector, length) + except Exception: + continue + else: + selector, length = self.keySelector('key', key) + self._keysToColumns( + columns, ('data', key), + doctype, getData, selector, length) + except Exception: + continue + + def _commonColumn(self, columns, keyname, doctype, getData, selector): + """ + Ensure that column with a commonly used key exists. + + :param columns: the column dictionary to possibly modify. + :param keyname: the key to the column. + :param doctype: the base document type. + :param getData: a function that, given the document record, returns the + data dictionary. + :param selector: a function that, given the document record, data + dictionary, and row, returns a value. + """ + title = self.commonColumns[keyname] + self._ensureColumn(columns, keyname, title, doctype, getData, selector, None) + + def _collectRecordRows( + self, record, data, selector, length, colkey, col, recidx, rows, + iid, aid, eid): + """ + Collect statistics and possible data from one data set. See + _collectRecords for parameter details. + """ + count = 0 + for rowidx in range(rows): + try: + value = selector(record, data, rowidx) + except Exception: + continue + if value is None or not isinstance(value, self.allowedTypes) or value == '': + continue + if col['type'] == 'number': + try: + value = float(value) + except Exception: + col['type'] = 'string' + col['distinct'] = {str(v) for v in col['distinct']} + col['count'] += 1 + if col['type'] == 'number': + if col['min'] is None: + col['min'] = col['max'] = value + col['min'] = min(col['min'], value) + col['max'] = max(col['max'], value) + else: + value = str(value) + if len(col['distinct']) <= self.maxDistinct: + col['distinct'].add(value) + if self._datacolumns and colkey in self._datacolumns: + self._datacolumns[colkey][( + iid, aid, eid, + rowidx if length is not None else -1)] = value + if not self._requiredColumns or colkey in self._requiredColumns: + count += 1 + return count + + def _collectRecords(self, columns, recordlist, doctype, iid='', aid=''): + """ + Collect statistics and possibly row values from a list of records. + + :param columns: the column dictionary to possibly modify. + :param recordlist: a list of records to use. + :param doctype: the base document type. + :param iid: an optional item id to use for determining distinct rows. + :param aid: an optional annotation id to use for determining distinct + rows. + :return: the number of required data entries added to the data + collection process. This will be zero when just listing columns. + If no required fields were specified, this will be the count of all + added data entries. + """ + count = 0 + eid = '' + for colkey, col in columns.items(): + if self._datacolumns and colkey not in self._datacolumns: + continue + for where, (getData, selector, length) in col['where'].items(): + if doctype != where and not where.startswith(doctype + '.'): + continue + for recidx, record in enumerate(recordlist): + if doctype == 'item': + iid = str(record['_id']) + elif doctype == 'annotation': + aid = str(record['_id']) + elif doctype == 'annotationelement': + eid = str(record['id']) + data = getData(record) + try: + rows = 1 if length is None else length(record, data) + except Exception: + continue + count += self._collectRecordRows( + record, data, selector, length, colkey, col, recidx, + rows, iid, aid, eid) + return count + + def _collectColumns(self, columns, recordlist, doctype, first=True, iid='', aid=''): + """ + Collect the columns available for a set of records. + + :param columns: the column dictionary to possibly modify. + :param recordlist: a list of records to use. + :param doctype: the base document type. + :param first: False if this is not the first page of a multi-page list + of records, + :param iid: an optional item id to use for determining distinct rows. + :param aid: an optional annotation id to use for determining distinct + rows. + :return: the number of required data entries added to the data + collection process. This will be zero when just listing columns. + If no required fields were specified, this will be the count of all + added data entries. + """ + getData = self.recordSelector(doctype.split('.', 1)[0]) + if doctype == 'item': + self._commonColumn(columns, 'item.id', doctype, getData, + lambda record, data, row: str(record['_id'])) + self._commonColumn(columns, 'item.name', doctype, getData, + lambda record, data, row: record['name']) + self._commonColumn(columns, 'item.description', doctype, getData, + lambda record, data, row: record['description']) + if doctype == 'annotation': + self._commonColumn(columns, 'annotation.id', doctype, getData, + lambda record, data, row: str(record['_id'])) + self._commonColumn(columns, 'annotation.name', doctype, getData, + lambda record, data, row: record['annotation']['name']) + self._commonColumn(columns, 'annotation.description', doctype, getData, + lambda record, data, row: record['annotation']['description']) + if doctype == 'annotationelement': + self._commonColumn(columns, 'annotationelement.id', doctype, getData, + lambda record, data, row: str(record['id'])) + self._commonColumn(columns, 'annotationelement.group', doctype, getData, + lambda record, data, row: record['group']) + self._commonColumn(columns, 'annotationelement.label', doctype, getData, + lambda record, data, row: record['label']['value']) + self._commonColumn(columns, 'annotationelement.type', doctype, getData, + lambda record, data, row: record['type']) + self._commonColumn(columns, 'bbox.x0', doctype, getData, + lambda record, data, row: record['_bbox']['lowx']) + self._commonColumn(columns, 'bbox.y0', doctype, getData, + lambda record, data, row: record['_bbox']['lowy']) + self._commonColumn(columns, 'bbox.x1', doctype, getData, + lambda record, data, row: record['_bbox']['highx']) + self._commonColumn(columns, 'bbox.y1', doctype, getData, + lambda record, data, row: record['_bbox']['highy']) + if first or self._fullScan or doctype != 'item': + for record in recordlist[:None if self._fullScan else 1]: + self._columnsFromData(columns, doctype, getData, record) + return self._collectRecords(columns, recordlist, doctype, iid, aid) + + def _getColumnsFromAnnotations(self, columns): + """ + Collect columns and data from annotations. + """ + from ..models.annotationelement import Annotationelement + + count = 0 + countsPerAnnotation = {} + for iidx, annotList in enumerate(self.annotations or []): + iid = str(self.items[iidx]['_id']) + for anidx, annot in enumerate(annotList): + # If the first item's annotation didn't contribute any required + # data to the data set, skip subsequent items' annotations; + # they are likely to be discarded. + if iidx and not countsPerAnnotation.get(anidx, 0) and not self._fullScan: + continue + startcount = count + if annot is None: + continue + if not self._sources or 'annotation' in self._sources: + count += self._collectColumns(columns, [annot], 'annotation', iid=iid) + # add annotation elements + if ((not self._sources or 'annotationelement' in self._sources) and + Annotationelement().countElements(annot) <= self.maxAnnotationElements): + for element in Annotationelement().yieldElements(annot, bbox=True): + count += self._collectColumns( + columns, [element], 'annotationelement', iid=iid, aid=str(annot['_id'])) + if not iidx: + countsPerAnnotation[anidx] = count - startcount + return count + + def _getColumnsFromDataFiles(self, columns): + """ + Collect columns and data from data files in items. + """ + if not len(self._itemfilelist) or not len(self._itemfilelist[0]): + return 0 + count = 0 + countsPerDataFile = {} + for iidx, dfList in enumerate(self._itemfilelist or []): + iid = str(self.items[iidx]['_id']) + for dfidx, file in enumerate(dfList): + # If the first item's data file didn't contribute any required + # data to the data set, skip subsequent items' data files; + # they are likely to be discarded. + if iidx and not countsPerDataFile.get(dfidx, 0) and not self._fullScan: + continue + startcount = count + if file is None: + continue + if not self._sources or 'datafile' in self._sources: + try: + df = _dfFromFile(file['_id'], bool(self._datacolumns or self._fullScan)) + count += self._collectColumns( + columns, [df] if isinstance(df, dict) else df, + f'datafile.{dfidx}', iid=iid) + except Exception: + logger.info( + f'Cannot process file {file["_id"]}: {file["name"]} as a dataframe') + raise + if not iidx: + countsPerDataFile[dfidx] = count - startcount + return count + + def _getColumns(self): + """ + Get a sorted list of plottable columns with some metadata for each. + + :returns: a sorted list of data entries. + """ + count = 0 + columns = {} + if not self._sources or 'folder' in self._sources: + count += self._collectColumns(columns, [self.folder], 'folder') + if not self._sources or 'item' in self._sources: + count += self._collectColumns(columns, self.items, 'item') + if self._moreItems: + for item in Folder().childItems( + self.folder, offset=len(self.items), **self._moreItems): + count += self._collectColumns(columns, [item], 'item', first=False) + count += self._getColumnsFromAnnotations(columns) + count += self._getColumnsFromDataFiles(columns) + for result in columns.values(): + if len(result['distinct']) <= self.maxDistinct: + result['distinct'] = sorted(result['distinct']) + result['distinctcount'] = len(result['distinct']) + else: + result.pop('distinct', None) + if result['type'] != 'number' or result['min'] is None: + result.pop('min', None) + result.pop('max', None) + prefixOrder = {'item': 0, 'annotation': 1, 'annotationelement': 2, 'data': 3, 'bbox': 4} + columns = sorted(columns.values(), key=lambda x: ( + prefixOrder.get(x['key'].split('.', 1)[0], len(prefixOrder)), x['key'])) + return columns + + @property + def columns(self): + """ + Get a sorted list of plottable columns with some metadata for each. + + Each data entry contains + + :key: the column key. For database entries, this is (item| + annotation|annotationelement).(id|name|description|group| + label). For bounding boxes this is bbox.(x0|y0|x1|y1). For + data from meta / attributes / user, this is + data.(key)[.0][.(key2)][.0] + :type: 'string' or 'number' + :title: a human readable title + :count: the number of non-null entries in the column + :[distinct]: a list of distinct values if there are less than some + maximum number of distinct values. This might not include + values from adjacent items + :[distinctcount]: if distinct is populated, this is len(distinct) + :[min]: for number data types, the lowest value present + :[max]: for number data types, the highest value present + + :returns: a sorted list of data entries. + """ + if self._columns is not None: + return self._columns + columns = self._getColumns() + self._columns = columns + return [{k: v for k, v in c.items() if k != 'where'} for c in self._columns if c['count']] + + def _collectData(self, rows, colsout): + """ + Get data rows and columns. + + :param rows: a list of row id tuples. + :param colsout: a list of output columns. + :returns: a data array and an updated row list. + """ + data = [[None] * len(colsout) for _ in range(len(rows))] + discard = set() + for cidx, col in enumerate(colsout): + colkey = col['key'] + if colkey in self._datacolumns: + datacol = self._datacolumns[colkey] + for ridx, rowid in enumerate(rows): + value = datacol.get(rowid, None) + if value is None and rowid[3] != -1: + value = datacol.get((rowid[0], rowid[1], rowid[2], -1), None) + if value is not None: + discard.add((rowid[0], rowid[1], rowid[2], -1)) + if value is None and (rowid[3] != -1 or rowid[2]): + value = datacol.get((rowid[0], rowid[1], '', -1), None) + if value is not None: + discard.add((rowid[0], rowid[1], '', -1)) + if value is None and (rowid[3] != -1 or rowid[2] or rowid[1]): + value = datacol.get((rowid[0], '', '', -1), None) + if value is not None: + discard.add((rowid[0], '', '', -1)) + if value is None and (rowid[3] != -1 or rowid[2] or rowid[1] or rowid[0]): + value = datacol.get(('', '', '', -1), None) + if value is not None: + discard.add(('', '', '', -1)) + data[ridx][cidx] = value + if len(discard): + data = [row for ridx, row in enumerate(data) if rows[ridx] not in discard] + rows = [row for ridx, row in enumerate(rows) if rows[ridx] not in discard] + return data, rows + +
+[docs] + def data(self, columns, requiredColumns=None): + """ + Get plottable data. + + :param columns: the columns to return. Either a list of column names + or a comma-delimited string. + :param requiredColumns: only return data rows where all of these + columns are non-None. Either a list of column names of a + comma-delimited string. + """ + if not isinstance(columns, list): + columns = columns.split(',') + if not isinstance(requiredColumns, list): + requiredColumns = requiredColumns.split(',') if requiredColumns is not None else [] + requiredColumns = set(requiredColumns) + self._requiredColumns = set(requiredColumns) + with self._dataLock: + self._datacolumns = {c: {} for c in columns} + rows = set() + # collects data as a side effect + collist = self._getColumns() + for coldata in self._datacolumns.values(): + rows |= set(coldata.keys()) + rows = sorted(rows) + colsout = [col.copy() for col in collist if col['key'] in columns] + for cidx, col in enumerate(colsout): + col['index'] = cidx + logger.info(f'Gathering {len(colsout)} x {len(rows)} data') + data, rows = self._collectData(rows, colsout) + self._datacolumns = None + for cidx, col in enumerate(colsout): + colkey = col['key'] + numrows = len(data) + if colkey in requiredColumns: + data = [row for row in data if row[cidx] is not None] + if len(data) < numrows: + logger.info(f'Reduced row count from {numrows} to {len(data)} ' + f'because of None values in column {colkey}') + subdata = data + for cidx, col in enumerate(colsout): + colkey = col['key'] + numrows = len(data) + if colkey in self._requiredColumns and colkey not in requiredColumns: + subdata = [row for row in subdata if row[cidx] is not None] + if len(subdata) and len(subdata) < len(data): + logger.info(f'Reduced row count from {len(data)} to {len(subdata)} ' + f'because of None values in implied columns') + data = subdata + # Refresh our count, distinct, distinctcount, min, max for each column + for cidx, col in enumerate(colsout): + col['count'] = len([row[cidx] for row in data if row[cidx] is not None]) + if col['type'] == 'number' and col['count']: + col['min'] = min(row[cidx] for row in data if row[cidx] is not None) + col['max'] = max(row[cidx] for row in data if row[cidx] is not None) + distinct = {str(row[cidx]) if col['type'] == 'string' else row[cidx] + for row in data if row[cidx] is not None} + if len(distinct) <= self.maxDistinct: + col['distinct'] = sorted(distinct) + col['distinctcount'] = len(distinct) + else: + col.pop('distinct', None) + col.pop('distinctcount', None) + colsout = [{k: v for k, v in c.items() if k != 'where'} for c in colsout] + return { + 'columns': colsout, + 'data': data}
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 000000000..c0d884cc4 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,196 @@ + + + + + + Overview: module code — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

All modules for which code is available

+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/cache_util/base.html b/_modules/large_image/cache_util/base.html new file mode 100644 index 000000000..67f0276fc --- /dev/null +++ b/_modules/large_image/cache_util/base.html @@ -0,0 +1,216 @@ + + + + + + large_image.cache_util.base — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.cache_util.base

+import hashlib
+import threading
+import time
+from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
+
+import cachetools
+
+_VT = TypeVar('_VT')
+
+
+
+[docs] +class BaseCache(cachetools.Cache): + """Base interface to cachetools.Cache for use with large-image.""" + + def __init__( + self, maxsize: float, + getsizeof: Optional[Callable[[_VT], float]] = None, + **kwargs) -> None: + super().__init__(maxsize=maxsize, getsizeof=getsizeof, **kwargs) + self.lastError: Dict[Tuple[Any, Callable], Dict[str, Any]] = {} + self.throttleErrors = 10 # seconds between logging errors + +
+[docs] + def logError(self, err: Any, func: Callable, msg: str) -> None: + """ + Log errors, but throttle them so as not to spam the logs. + + :param err: error to log. + :param func: function to use for logging. This is something like + logprint.exception or logger.error. + :param msg: the message to log. + """ + curtime = time.time() + key = (err, func) + if (curtime - self.lastError.get(key, {}).get('time', 0) > self.throttleErrors): + skipped = self.lastError.get(key, {}).get('skipped', 0) + if skipped: + msg += ' (%d similar messages)' % skipped + self.lastError[key] = {'time': curtime, 'skipped': 0} + func(msg) + else: + self.lastError[key]['skipped'] += 1
+ + + def __repr__(self): + raise NotImplementedError + + def __iter__(self): + raise NotImplementedError + + def __len__(self) -> int: + raise NotImplementedError + + def __contains__(self, item) -> bool: + raise NotImplementedError + + def __delitem__(self, key): + raise NotImplementedError + + def _hashKey(self, key) -> str: + return hashlib.sha256(key.encode()).hexdigest() + + def __getitem__(self, key): + # hashedKey = self._hashKey(key) + raise NotImplementedError + + def __setitem__(self, key, value): + # hashedKey = self._hashKey(key) + raise NotImplementedError + + @property + def curritems(self) -> int: + raise NotImplementedError + + @property + def currsize(self) -> int: + raise NotImplementedError + + @property + def maxsize(self) -> int: + raise NotImplementedError + +
+[docs] + def clear(self) -> None: + raise NotImplementedError
+ + +
+[docs] + @staticmethod + def getCache() -> Tuple[Optional['BaseCache'], threading.Lock]: + # return cache, cacheLock + raise NotImplementedError
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/cache_util/cache.html b/_modules/large_image/cache_util/cache.html new file mode 100644 index 000000000..711765212 --- /dev/null +++ b/_modules/large_image/cache_util/cache.html @@ -0,0 +1,416 @@ + + + + + + large_image.cache_util.cache — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.cache_util.cache

+import functools
+import pickle
+import threading
+import uuid
+from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
+
+import cachetools
+from typing_extensions import ParamSpec
+
+try:
+    import resource
+    HAS_RESOURCE = True
+except ImportError:
+    HAS_RESOURCE = False
+
+from .. import config
+from .cachefactory import CacheFactory, pickAvailableCache
+
+P = ParamSpec('P')
+T = TypeVar('T')
+
+_tileCache: Optional[cachetools.Cache] = None
+_tileLock: Optional[threading.Lock] = None
+
+_cacheLockKeyToken = '_cacheLock_key'
+
+# If we have a resource module, ask to use as many file handles as the hard
+# limit allows, then calculate how may tile sources we can have open based on
+# the actual limit.
+MaximumTileSources = 10
+if HAS_RESOURCE:
+    try:
+        SoftNoFile, HardNoFile = resource.getrlimit(resource.RLIMIT_NOFILE)
+        resource.setrlimit(resource.RLIMIT_NOFILE, (HardNoFile, HardNoFile))
+        SoftNoFile, HardNoFile = resource.getrlimit(resource.RLIMIT_NOFILE)
+        # Reserve some file handles for general use, and expect that tile
+        # sources could use many handles each.  This is conservative, since
+        # running out of file handles breaks the program in general.
+        MaximumTileSources = max(3, (SoftNoFile - 10) // 20)
+    except Exception:
+        pass
+
+
+CacheProperties = {
+    'tilesource': {
+        # Cache size is based on what the class needs, which does not include
+        # individual tiles
+        'itemExpectedSize': 24 * 1024 ** 2,
+        'maxItems': MaximumTileSources,
+        # The cache timeout is not currently being used, but it is set here in
+        # case we ever choose to implement it.
+        'cacheTimeout': 300,
+    },
+}
+
+
+
+[docs] +def strhash(*args, **kwargs) -> str: + """ + Generate a string hash value for an arbitrary set of args and kwargs. This + relies on the repr of each element. + + :param args: arbitrary tuple of args. + :param kwargs: arbitrary dictionary of kwargs. + :returns: hashed string of the arguments. + """ + if kwargs: + return '%r,%r' % (args, sorted(kwargs.items())) + return repr(args)
+ + + +
+[docs] +def methodcache(key: Optional[Callable] = None) -> Callable: # noqa + """ + Decorator to wrap a function with a memoizing callable that saves results + in self.cache. This is largely taken from cachetools, but uses a cache + from self.cache rather than a passed value. If self.cache_lock is + present and not none, a lock is used. + + :param key: if a function, use that for the key, otherwise use self.wrapKey. + """ + def decorator(func: Callable[P, T]) -> Callable[..., T]: + @functools.wraps(func) + def wrapper(self, *args: P.args, **kwargs: P.kwargs) -> T: + k = key(*args, **kwargs) if key else self.wrapKey(*args, **kwargs) + lock = getattr(self, 'cache_lock', None) + ck = getattr(self, '_classkey', None) + if lock: + with self.cache_lock: + if hasattr(self, '_classkeyLock'): + if self._classkeyLock.acquire(blocking=False): + self._classkeyLock.release() + else: + ck = getattr(self, '_unlocked_classkey', ck) + if ck: + k = ck + ' ' + k + try: + if lock: + with self.cache_lock: + return self.cache[k] + else: + return self.cache[k] + except KeyError: + pass # key not found + except (ValueError, pickle.UnpicklingError): + # this can happen if a different version of python wrote the record + pass + v = func(self, *args, **kwargs) + try: + if lock: + with self.cache_lock: + self.cache[k] = v + else: + self.cache[k] = v + except ValueError: + pass # value too large + except (KeyError, RuntimeError): + # the key was refused for some reason + config.getLogger().debug( + 'Had a cache KeyError while trying to store a value to key %r' % (k)) + return v + return wrapper + return decorator
+ + + +
+[docs] +class LruCacheMetaclass(type): + namedCaches: Dict[str, Any] = {} + classCaches: Dict[type, Any] = {} + + def __new__(metacls, name, bases, namespace, **kwargs): + # Get metaclass parameters by finding and removing them from the class + # namespace (necessary for Python 2), or preferentially as metaclass + # arguments (only in Python 3). + cacheName = namespace.get('cacheName', None) + cacheName = kwargs.get('cacheName', cacheName) + + maxSize = CacheProperties.get(cacheName, {}).get('cacheMaxSize', None) + if (maxSize is None and cacheName in CacheProperties and + 'maxItems' in CacheProperties[cacheName] and + CacheProperties[cacheName].get('itemExpectedSize')): + maxSize = pickAvailableCache( + CacheProperties[cacheName]['itemExpectedSize'], + maxItems=CacheProperties[cacheName]['maxItems'], + cacheName=cacheName) + maxSize = namespace.pop('cacheMaxSize', maxSize) + maxSize = kwargs.get('cacheMaxSize', maxSize) + if maxSize is None: + raise TypeError('Usage of the LruCacheMetaclass requires a ' + '"cacheMaxSize" attribute on the class %s.' % name) + + timeout = CacheProperties.get(cacheName, {}).get('cacheTimeout', None) + timeout = namespace.pop('cacheTimeout', timeout) + timeout = kwargs.get('cacheTimeout', timeout) + + cls = super().__new__( + metacls, name, bases, namespace) + if not cacheName: + cacheName = cls + + if LruCacheMetaclass.namedCaches.get(cacheName) is None: + cache, cacheLock = CacheFactory().getCache( + numItems=maxSize, + cacheName=cacheName, + inProcess=True, + ) + LruCacheMetaclass.namedCaches[cacheName] = (cache, cacheLock) + config.getLogger().debug( + 'Created LRU Cache for %r with %d maximum size' % (cacheName, cache.maxsize)) + else: + (cache, cacheLock) = LruCacheMetaclass.namedCaches[cacheName] + + # Don't store the cache in cls.__dict__, because we don't want it to be + # part of the attribute lookup hierarchy + # TODO: consider putting it in cls.__dict__, to inspect statistics + # cls is hashable though, so use it to lookup the cache, in case an + # identically-named class gets redefined + LruCacheMetaclass.classCaches[cls] = (cache, cacheLock) + + return cls + + def __call__(cls, *args, **kwargs) -> Any: # noqa - N805 + if kwargs.get('noCache') or ( + kwargs.get('noCache') is None and config.getConfig('cache_sources') is False): + instance = super().__call__(*args, **kwargs) + # for pickling + instance._initValues = (args, kwargs.copy()) + instance._classkey = str(uuid.uuid4()) + instance._noCache = True + if kwargs.get('style') != getattr(cls, '_unstyledStyle', None): + subkwargs = kwargs.copy() + subkwargs['style'] = getattr(cls, '_unstyledStyle', None) + instance._unstyledInstance = subresult = cls(*args, **subkwargs) + return instance + cache, cacheLock = LruCacheMetaclass.classCaches[cls] + + if hasattr(cls, 'getLRUHash'): + key = cls.getLRUHash(*args, **kwargs) + else: + key = strhash(args[0], kwargs) + key = cls.__name__ + ' ' + key + with cacheLock: + try: + result = cache[key] + if (not isinstance(result, tuple) or len(result) != 2 or + result[0] != _cacheLockKeyToken): + return result + cacheLockForKey = result[1] + except KeyError: + # By passing and handling the cache miss outside of the + # exception, any exceptions while trying to populate the cache + # will not be reported in the cache exception context. + cacheLockForKey = threading.Lock() + cache[key] = (_cacheLockKeyToken, cacheLockForKey) + with cacheLockForKey: + with cacheLock: + try: + result = cache[key] + if (not isinstance(result, tuple) or len(result) != 2 or + result[0] != _cacheLockKeyToken): + return result + except KeyError: + pass + try: + # This conditionally copies a non-styled class and adds a style. + if (kwargs.get('style') and hasattr(cls, '_setStyle') and + kwargs.get('style') != getattr(cls, '_unstyledStyle', None)): + subkwargs = kwargs.copy() + subkwargs['style'] = getattr(cls, '_unstyledStyle', None) + subresult = cls(*args, **subkwargs) + result = subresult.__class__.__new__(subresult.__class__) + with subresult._sourceLock: + result.__dict__ = subresult.__dict__.copy() + result._sourceLock = threading.RLock() + result._classkey = key + # for pickling + result._initValues = (args, kwargs.copy()) + result._unstyledInstance = subresult + result._derivedSource = True + # Has to be after setting the _unstyledInstance + result._setStyle(kwargs['style']) + with cacheLock: + cache[key] = result + return result + instance = super().__call__(*args, **kwargs) + # for pickling + instance._initValues = (args, kwargs.copy()) + except Exception as exc: + with cacheLock: + try: + del cache[key] + except Exception: + pass + raise exc + instance._classkey = key + if kwargs.get('style') != getattr(cls, '_unstyledStyle', None): + subkwargs = kwargs.copy() + subkwargs['style'] = getattr(cls, '_unstyledStyle', None) + instance._unstyledInstance = subresult = cls(*args, **subkwargs) + instance._derivedSource = True + with cacheLock: + cache[key] = instance + return instance
+ + + +
+[docs] +def getTileCache() -> Tuple[cachetools.Cache, Optional[threading.Lock]]: + """ + Get the preferred tile cache and lock. + + :returns: tileCache and tileLock. + """ + global _tileCache, _tileLock + + if _tileCache is None: + # Decide whether to use Memcached or cachetools + _tileCache, _tileLock = CacheFactory().getCache(cacheName='tileCache') + return _tileCache, _tileLock
+ + + +
+[docs] +def isTileCacheSetup() -> bool: + """ + Return True if the tile cache has been created. + + :returns: True if _tileCache is not None. + """ + return _tileCache is not None
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/cache_util/cachefactory.html b/_modules/large_image/cache_util/cachefactory.html new file mode 100644 index 000000000..f31e4da0f --- /dev/null +++ b/_modules/large_image/cache_util/cachefactory.html @@ -0,0 +1,306 @@ + + + + + + large_image.cache_util.cachefactory — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for large_image.cache_util.cachefactory

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import math
+import threading
+from importlib.metadata import entry_points
+from typing import Dict, Optional, Tuple, Type
+
+import cachetools
+
+from .. import config
+from ..exceptions import TileCacheError
+from .memcache import MemCache
+from .rediscache import RedisCache
+
+# DO NOT MANUALLY ADD ANYTHING TO `_availableCaches`
+#  use entrypoints and let loadCaches fill in `_availableCaches`
+_availableCaches: Dict[str, Type[cachetools.Cache]] = {}
+
+
+
+[docs] +def loadCaches( + entryPointName: str = 'large_image.cache', + sourceDict: Dict[str, Type[cachetools.Cache]] = _availableCaches) -> None: + """ + Load all caches from entrypoints and add them to the + availableCaches dictionary. + + :param entryPointName: the name of the entry points to load. + :param sourceDict: a dictionary to populate with the loaded caches. + """ + if len(_availableCaches): + return + epoints = entry_points() + # Python 3.10 uses select and deprecates dictionary interface + epointList = epoints.select(group=entryPointName) if hasattr( + epoints, 'select') else epoints.get(entryPointName, []) + for entryPoint in epointList: + try: + cacheClass = entryPoint.load() + sourceDict[entryPoint.name.lower()] = cacheClass + config.getLogger('logprint').debug(f'Loaded cache {entryPoint.name}') + except Exception: + config.getLogger('logprint').exception( + f'Failed to load cache {entryPoint.name}', + ) + # Load memcached last for now + if MemCache is not None: + # TODO: put this in an entry point for a new package + _availableCaches['memcached'] = MemCache + if RedisCache is not None: + _availableCaches['redis'] = RedisCache
+ + # NOTE: `python` cache is viewed as a fallback and isn't listed in `availableCaches` + + +
+[docs] +def pickAvailableCache( + sizeEach: int, portion: int = 8, maxItems: Optional[int] = None, + cacheName: Optional[str] = None) -> int: + """ + Given an estimated size of an item, return how many of those items would + fit in a fixed portion of the available virtual memory. + + :param sizeEach: the expected size of an item that could be cached. + :param portion: the inverse fraction of the memory which can be used. + :param maxItems: if specified, the number of items is never more than this + value. + :param cacheName: if specified, the portion can be affected by the + configuration. + :return: the number of items that should be cached. Always at least two, + unless maxItems is less. + """ + if cacheName: + portion = max(portion, int(config.getConfig( + f'cache_{cacheName}_memory_portion', portion))) + configMaxItems = int(config.getConfig(f'cache_{cacheName}_maximum', 0)) + if configMaxItems > 0: + maxItems = configMaxItems + # Estimate usage based on (1 / portion) of the total virtual memory. + memory = config.total_memory() + numItems = max(int(math.floor(memory / portion / sizeEach)), 2) + if maxItems: + numItems = min(numItems, maxItems) + return numItems
+ + + +
+[docs] +def getFirstAvailableCache() -> Tuple[cachetools.Cache, Optional[threading.Lock]]: + cacheBackend = config.getConfig('cache_backend', None) + if cacheBackend is not None: + msg = 'cache_backend already set' + raise ValueError(msg) + loadCaches() + cache, cacheLock = None, None + for cacheBackend in _availableCaches: + try: + cache, cacheLock = _availableCaches[cacheBackend].getCache() # type: ignore + break + except TileCacheError: + continue + if cache is not None: + config.getLogger('logprint').debug( + f'Automatically setting `{cacheBackend}` as cache_backend from availableCaches', + ) + config.setConfig('cache_backend', cacheBackend) + return cache, cacheLock
+ + + +
+[docs] +class CacheFactory: + logged = False + +
+[docs] + def getCacheSize(self, numItems: Optional[int], cacheName: Optional[str] = None) -> int: + if numItems is None: + defaultPortion = 32 + try: + portion = int(config.getConfig('cache_python_memory_portion', 0)) + if cacheName: + portion = max(portion, int(config.getConfig( + f'cache_{cacheName}_memory_portion', portion))) + portion = max(portion or defaultPortion, 3) + except ValueError: + portion = defaultPortion + numItems = pickAvailableCache(256**2 * 4 * 2, portion) + if cacheName: + try: + maxItems = int(config.getConfig(f'cache_{cacheName}_maximum', 0)) + if maxItems > 0: + numItems = min(numItems, max(maxItems, 3)) + except ValueError: + pass + return numItems
+ + +
+[docs] + def getCache( + self, numItems: Optional[int] = None, + cacheName: Optional[str] = None, + inProcess: bool = False) -> Tuple[cachetools.Cache, Optional[threading.Lock]]: + loadCaches() + + # Default to `python` cache for inProcess + cacheBackend = config.getConfig('cache_backend', 'python' if inProcess else None) + + if isinstance(cacheBackend, str): + cacheBackend = cacheBackend.lower() + + cache = None + if not inProcess and cacheBackend in _availableCaches: + cache, cacheLock = _availableCaches[cacheBackend].getCache() # type: ignore + elif not inProcess and cacheBackend is None: + cache, cacheLock = getFirstAvailableCache() + + if cache is None: # fallback backend or inProcess + cacheBackend = 'python' + cache = cachetools.LRUCache(self.getCacheSize(numItems, cacheName=cacheName)) + cacheLock = threading.Lock() + + if not inProcess and not CacheFactory.logged: + config.getLogger('logprint').debug(f'Using {cacheBackend} for large_image caching') + CacheFactory.logged = True + + return cache, cacheLock
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/cache_util/memcache.html b/_modules/large_image/cache_util/memcache.html new file mode 100644 index 000000000..085e262c9 --- /dev/null +++ b/_modules/large_image/cache_util/memcache.html @@ -0,0 +1,315 @@ + + + + + + large_image.cache_util.memcache — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for large_image.cache_util.memcache

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import copy
+import threading
+import time
+from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union
+
+from .. import config
+from .base import BaseCache
+
+_VT = TypeVar('_VT')
+
+
+
+[docs] +class MemCache(BaseCache): + """Use memcached as the backing cache.""" + + def __init__( + self, url: Union[str, List[str]] = '127.0.0.1', + username: Optional[str] = None, password: Optional[str] = None, + getsizeof: Optional[Callable[[_VT], float]] = None, + mustBeAvailable: bool = False) -> None: + import pylibmc + + self.pylibmc = pylibmc + super().__init__(0, getsizeof=getsizeof) + if isinstance(url, str): + url = [url] + # pylibmc used to connect to memcached client. Set failover behavior. + # See http://sendapatch.se/projects/pylibmc/behaviors.html + behaviors = { + 'tcp_nodelay': True, + 'ketama': True, + 'no_block': True, + 'retry_timeout': 1, + 'dead_timeout': 10, + } + # Adding remove_failed prevents recovering in a single memcached server + # instance, so only do it if there are multiple servers + if len(url) > 1: + behaviors['remove_failed'] = 1 + # name mangling to override 'private variable' __data in cache + self._clientParams = (url, dict( + binary=True, username=username, password=password, behaviors=behaviors)) + self._client = pylibmc.Client(self._clientParams[0], **self._clientParams[1]) + if mustBeAvailable: + # Try to set a value; this will throw an error if the server is + # unreachable, so we don't bother trying to use it. + self._client['large_image_cache_test'] = time.time() + + def __repr__(self) -> str: + return "Memcache doesn't list its keys" + + def __iter__(self): + # return invalid iter + return None + + def __len__(self) -> int: + # return invalid length + return -1 + + def __contains__(self, item: object) -> bool: + # cache never contains key + return False + + def __delitem__(self, key: str) -> None: + hashedKey = self._hashKey(key) + del self._client[hashedKey] + + def __getitem__(self, key: str) -> Any: + hashedKey = self._hashKey(key) + try: + return self._client[hashedKey] + except KeyError: + return self.__missing__(key) + except self.pylibmc.ServerDown: + self.logError(self.pylibmc.ServerDown, config.getLogger('logprint').info, + 'Memcached ServerDown') + self._reconnect() + return self.__missing__(key) + except self.pylibmc.Error: + self.logError(self.pylibmc.Error, config.getLogger('logprint').exception, + 'pylibmc exception') + return self.__missing__(key) + + def __setitem__(self, key: str, value: Any) -> None: + hashedKey = self._hashKey(key) + try: + self._client[hashedKey] = value + except (TypeError, KeyError) as exc: + valueSize = value.shape if hasattr(value, 'shape') else ( + value.size if hasattr(value, 'size') else ( + len(value) if hasattr(value, '__len__') else None)) + valueRepr = repr(value) + if len(valueRepr) > 500: + valueRepr = valueRepr[:500] + '...' + self.logError( + exc.__class__, config.getLogger('logprint').error, + '%s: Failed to save value (size %r) with key %s' % ( + exc.__class__.__name__, valueSize, hashedKey)) + except self.pylibmc.ServerDown: + self.logError(self.pylibmc.ServerDown, config.getLogger('logprint').info, + 'Memcached ServerDown') + self._reconnect() + except self.pylibmc.TooBig: + pass + except self.pylibmc.Error as exc: + # memcached won't cache items larger than 1 Mb (or a configured + # size), but this returns a 'SUCCESS' error. Raise other errors. + if 'SUCCESS' not in repr(exc.args): + self.logError(self.pylibmc.Error, config.getLogger('logprint').exception, + 'pylibmc exception') + + @property + def curritems(self) -> int: + return self._getStat('curr_items') + + @property + def currsize(self) -> int: + return self._getStat('bytes') + + @property + def maxsize(self) -> int: + return self._getStat('limit_maxbytes') + + def _reconnect(self) -> None: + try: + self._lastReconnectBackoff = getattr(self, '_lastReconnectBackoff', 2) + if time.time() - getattr(self, '_lastReconnect', 0) > self._lastReconnectBackoff: + config.getLogger('logprint').info('Trying to reconnect to memcached server') + self._client = self.pylibmc.Client(self._clientParams[0], **self._clientParams[1]) + self._lastReconnectBackoff = min(self._lastReconnectBackoff + 1, 30) + self._lastReconnect = time.time() + except Exception: + pass + + def _blockingClient(self) -> Any: + params = copy.deepcopy(self._clientParams) + params[1]['behaviors']['no_block'] = False # type: ignore + return self.pylibmc.Client(params[0], **params[1]) + + def _getStat(self, key: str) -> int: + try: + stats = self._blockingClient().get_stats() + value = sum(int(s[key]) for server, s in stats) + except Exception: + return 0 + return value + +
+[docs] + def clear(self) -> None: + self._client.flush_all()
+ + +
+[docs] + @staticmethod + def getCache() -> Tuple[Optional['MemCache'], threading.Lock]: + # lock needed because pylibmc(memcached client) is not threadsafe + cacheLock = threading.Lock() + + # check if credentials and location exist, otherwise assume + # location is 127.0.0.1 (localhost) with no password + url = config.getConfig('cache_memcached_url') + if not url: + url = '127.0.0.1' + memcachedUsername = config.getConfig('cache_memcached_username') + if not memcachedUsername: + memcachedUsername = None + memcachedPassword = config.getConfig('cache_memcached_password') + if not memcachedPassword: + memcachedPassword = None + try: + cache = MemCache(url, memcachedUsername, memcachedPassword, + mustBeAvailable=True) + except Exception: + config.getLogger().info('Cannot use memcached for caching.') + cache = None + return cache, cacheLock
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/cache_util/rediscache.html b/_modules/large_image/cache_util/rediscache.html new file mode 100644 index 000000000..3118e5ed6 --- /dev/null +++ b/_modules/large_image/cache_util/rediscache.html @@ -0,0 +1,304 @@ + + + + + + large_image.cache_util.rediscache — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for large_image.cache_util.rediscache

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import pickle
+import threading
+import time
+from typing import Any, Callable, Iterable, List, Optional, Sized, Tuple, TypeVar, Union, cast
+
+from typing_extensions import Buffer
+
+from .. import config
+from .base import BaseCache
+
+_VT = TypeVar('_VT')
+
+
+
+[docs] +class RedisCache(BaseCache): + """Use redis as the backing cache.""" + + def __init__( + self, url: Union[str, List[str]] = '127.0.0.1:6379', + username: Optional[str] = None, password: Optional[str] = None, + getsizeof: Optional[Callable[[_VT], float]] = None, + mustBeAvailable: bool = False) -> None: + import redis + from redis.client import Redis + + self.redis = redis + self._redisCls = Redis + super().__init__(0, getsizeof=getsizeof) + self._cache_key_prefix = 'large_image_' + self._clientParams = (f'redis://{url}', dict( + username=username, password=password, db=0, retry_on_timeout=1)) + self._client: Redis = Redis.from_url(self._clientParams[0], **self._clientParams[1]) + if mustBeAvailable: + # Try to ping server; this will throw an error if the server is + # unreachable, so we don't bother trying to use it. + self._client.ping() + + def __repr__(self) -> str: + return "Redis doesn't list its keys" + + def __iter__(self): + # return invalid iter + return None + + def __len__(self) -> int: + # return invalid length + keys = self._client.keys(f'{self._cache_key_prefix}*') + return len(cast(Sized, keys)) + + def __contains__(self, key) -> bool: + # cache never contains key + _key = self._cache_key_prefix + self._hashKey(key) + return bool(self._client.exists(_key)) + + def __delitem__(self, key: str) -> None: + if not self.__contains__(key): + raise KeyError + _key = self._cache_key_prefix + self._hashKey(key) + self._client.delete(_key) + + def __getitem__(self, key: str) -> Any: + _key = self._cache_key_prefix + self._hashKey(key) + try: + # must determine if tke key exists , otherwise cache_test can not be passed. + if not self.__contains__(key): + raise KeyError + return pickle.loads(cast(Buffer, self._client.get(_key))) + except KeyError: + return self.__missing__(key) + except self.redis.ConnectionError: + self.logError(self.redis.ConnectionError, config.getLogger('logprint').info, + 'redis ConnectionError') + self._reconnect() + return self.__missing__(key) + except self.redis.RedisError: + self.logError(self.redis.RedisError, config.getLogger('logprint').exception, + 'redis RedisError') + return self.__missing__(key) + + def __setitem__(self, key: str, value: Any) -> None: + _key = self._cache_key_prefix + self._hashKey(key) + try: + self._client.set(_key, pickle.dumps(value)) + except (TypeError, KeyError) as exc: + valueSize = value.shape if hasattr(value, 'shape') else ( + value.size if hasattr(value, 'size') else ( + len(value) if hasattr(value, '__len__') else None)) + valueRepr = repr(value) + if len(valueRepr) > 500: + valueRepr = valueRepr[:500] + '...' + self.logError( + exc.__class__, config.getLogger('logprint').error, + '%s: Failed to save value (size %r) with key %s' % ( + exc.__class__.__name__, valueSize, key)) + except self.redis.ConnectionError: + self.logError(self.redis.ConnectionError, config.getLogger('logprint').info, + 'redis ConnectionError') + self._reconnect() + + @property + def curritems(self) -> int: + return cast(int, self._client.dbsize()) + + @property + def currsize(self) -> int: + return self._getStat('used_memory') + + @property + def maxsize(self) -> int: + maxmemory = self._getStat('maxmemory') + if maxmemory: + return maxmemory + else: + return self._getStat('total_system_memory') + + def _reconnect(self) -> None: + try: + self._lastReconnectBackoff = getattr(self, '_lastReconnectBackoff', 2) + if time.time() - getattr(self, '_lastReconnect', 0) > self._lastReconnectBackoff: + config.getLogger('logprint').info('Trying to reconnect to redis server') + self._client = self._redisCls.from_url(self._clientParams[0], + **self._clientParams[1]) + self._lastReconnectBackoff = min(self._lastReconnectBackoff + 1, 30) + self._lastReconnect = time.time() + except Exception: + pass + + def _getStat(self, key: str) -> int: + try: + stats = self._client.info() + value = cast(dict, stats)[key] + except Exception: + return 0 + return value + +
+[docs] + def clear(self) -> None: + keys = self._client.keys(f'{self._cache_key_prefix}*') + if keys: + self._client.delete(*list(cast(Iterable[Any], keys)))
+ + +
+[docs] + @staticmethod + def getCache() -> Tuple[Optional['RedisCache'], threading.Lock]: + cacheLock = threading.Lock() + + # check if credentials and location exist, otherwise assume + # location is 127.0.0.1 (localhost) with no password + url = config.getConfig('cache_redis_url') + if not url: + url = '127.0.0.1:6379' + redisUsername = config.getConfig('cache_redis_username') + if not redisUsername: + redisUsername = None + redisPassword = config.getConfig('cache_redis_password') + if not redisPassword: + redisPassword = None + try: + cache = RedisCache(url, redisUsername, redisPassword, + mustBeAvailable=True) + except Exception: + config.getLogger().info('Cannot use redis for caching.') + cache = None + return cache, cacheLock
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/config.html b/_modules/large_image/config.html new file mode 100644 index 000000000..107959297 --- /dev/null +++ b/_modules/large_image/config.html @@ -0,0 +1,337 @@ + + + + + + large_image.config — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.config

+import functools
+import json
+import logging
+import os
+import pathlib
+import re
+from typing import Any, Optional, Union, cast
+
+from . import exceptions
+
+try:
+    import psutil
+    HAS_PSUTIL = True
+except ImportError:
+    HAS_PSUTIL = False
+
+# Default logger
+fallbackLogger = logging.getLogger('large_image')
+fallbackLogger.setLevel(logging.INFO)
+fallbackLogHandler = logging.NullHandler()
+fallbackLogHandler.setLevel(logging.NOTSET)
+fallbackLogger.addHandler(fallbackLogHandler)
+
+ConfigValues = {
+    'logger': fallbackLogger,
+    'logprint': fallbackLogger,
+
+    # For tiles
+    'cache_backend': None,  # 'python', 'redis' or 'memcached'
+    # 'python' cache can use 1/(val) of the available memory
+    'cache_python_memory_portion': 32,
+    # cache_memcached_url may be a list
+    'cache_memcached_url': '127.0.0.1',
+    'cache_memcached_username': None,
+    'cache_memcached_password': None,
+    'cache_redis_url': '127.0.0.1:6379',
+    'cache_redis_password': None,
+
+    # If set to False, the default will be to not cache tile sources.  This has
+    # substantial performance penalties if sources are used multiple times, so
+    # should only be set in singular dynamic environments such as experimental
+    # notebooks.
+    'cache_sources': True,
+
+    # Generally, these keys are the form of "cache_<cacheName>_<key>"
+
+    # For tilesources.  These are also limited by available file handles.
+    # 'python' cache can use 1/(val) of the available memory based on a very
+    # rough estimate of the amount of memory used by a tilesource
+    'cache_tilesource_memory_portion': 16,
+    # If >0, this is the maximum number of tilesources that will be cached
+    'cache_tilesource_maximum': 0,
+
+    'max_small_image_size': 4096,
+
+    # Should ICC color correction be applied by default
+    'icc_correction': True,
+
+    # The maximum size of an annotation file that will be ingested into girder
+    # via direct load
+    'max_annotation_input_file_length': 1 * 1024 ** 3 if not HAS_PSUTIL else max(
+        1 * 1024 ** 3, psutil.virtual_memory().total // 16),
+
+    # Any path that matches here will only be opened by a source that matches
+    # extension or mime type.
+    'all_sources_ignored_names': r'(\.mrxs|\.vsi)$',
+}
+
+
+# Fix when we drop Python 3.8 to just be @functools.cache
+
+[docs] +@functools.lru_cache(maxsize=None) +def getConfig(key: Optional[str] = None, + default: Optional[Union[str, bool, int, logging.Logger]] = None) -> Any: + """ + Get the config dictionary or a value from the cache config settings. + + :param key: if None, return the config dictionary. Otherwise, return the + value of the key if it is set or the default value if it is not. + :param default: a value to return if a key is requested and not set. + :returns: either the config dictionary or the value of a key. + """ + if key is None: + return ConfigValues + envKey = f'LARGE_IMAGE_{key.replace(".", "_").upper()}' + if envKey in os.environ: + value = os.environ[envKey] + if value == '__default__': + return default + try: + value = json.loads(value) + except ValueError: + pass + return value + return ConfigValues.get(key, default)
+ + + +
+[docs] +def getLogger(key: Optional[str] = None, + default: Optional[logging.Logger] = None) -> logging.Logger: + """ + Get a logger from the config. Ensure that it is a valid logger. + + :param key: if None, return the 'logger'. + :param default: a value to return if a key is requested and not set. + :returns: a logger. + """ + logger = cast(logging.Logger, getConfig(key or 'logger', default)) + if not isinstance(logger, logging.Logger): + logger = fallbackLogger + return logger
+ + + +
+[docs] +def setConfig(key: str, value: Optional[Union[str, bool, int, logging.Logger]]) -> None: + """ + Set a value in the config settings. + + :param key: the key to set. + :param value: the value to store in the key. + """ + curConfig = getConfig() + if curConfig.get(key) is not value: + curConfig[key] = value + getConfig.cache_clear()
+ + + +def _ignoreSourceNames( + configKey: str, path: Union[str, pathlib.Path], default: Optional[str] = None) -> None: + """ + Given a path, if it is an actual file and there is a setting + "source_<configKey>_ignored_names", raise a TileSourceError if the path + matches the ignore names setting regex in a case-insensitive search. + + :param configKey: key to use to fetch value from settings. + :param path: the file path to check. + :param default: a default ignore regex, or None for no default. + """ + ignored_names = getConfig('source_%s_ignored_names' % configKey) or default + if not ignored_names or not os.path.isfile(path): + return None + if re.search(ignored_names, os.path.basename(path), flags=re.IGNORECASE): + raise exceptions.TileSourceError('File will not be opened by %s reader' % configKey) + + +
+[docs] +def cpu_count(logical: bool = True) -> int: + """ + Get the usable CPU count. If psutil is available, it is used, since it can + determine the number of physical CPUS versus logical CPUs. This returns + the smaller of that value from psutil and the number of cpus allowed by the + os scheduler, which means that for physical requests (logical=False), the + returned value may be more the the number of physical cpus that are usable. + + :param logical: True to get the logical usable CPUs (which include + hyperthreading). False for the physical usable CPUs. + :returns: the number of usable CPUs. + """ + count = os.cpu_count() or 2 + try: + count = min(count, len(os.sched_getaffinity(0))) + except AttributeError: + pass + try: + import psutil + + count = min(count, psutil.cpu_count(logical)) + except ImportError: + pass + return max(1, count)
+ + + +
+[docs] +def total_memory() -> int: + """ + Get the total memory in the system. If this is in a container, try to + determine the memory available to the cgroup. + + :returns: the available memory in bytes, or 8 GB if unknown. + """ + mem = 0 + if HAS_PSUTIL: + mem = psutil.virtual_memory().total + try: + cgroup = int(open('/sys/fs/cgroup/memory/memory.limit_in_bytes').read().strip()) + if 1024 ** 3 <= cgroup < 1024 ** 4 and (mem is None or cgroup < mem): + mem = cgroup + except Exception: + pass + if mem: + return mem + return 8 * 1024 ** 3
+ + + +
+[docs] +def minimizeCaching(mode=None): + """ + Set python cache sizes to very low values. + + :param mode: None for all caching, 'tile' for the tile cache, 'source' for + the source cache. + """ + if not mode or str(mode).lower().startswith('source'): + setConfig('cache_tilesource_maximum', 1) + if not mode or str(mode).lower().startswith('tile'): + setConfig('cache_backend', 'python') + setConfig('cache_python_memory_portion', 256)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/constants.html b/_modules/large_image/constants.html new file mode 100644 index 000000000..c97c210fd --- /dev/null +++ b/_modules/large_image/constants.html @@ -0,0 +1,214 @@ + + + + + + large_image.constants — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.constants

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import enum
+
+
+
+[docs] +class SourcePriority(enum.IntEnum): + NAMED = 0 # Explicitly requested + PREFERRED = 1 + HIGHER = 2 + HIGH = 3 + MEDIUM = 4 + LOW = 5 + LOWER = 6 + IMPLICIT_HIGH = 7 + IMPLICIT = 8 + FALLBACK_HIGH = 9 + FALLBACK = 10 + MANUAL = 11 # This and higher values will never be selected automatically
+ + + +TILE_FORMAT_IMAGE = 'image' +TILE_FORMAT_PIL = 'PIL' +TILE_FORMAT_NUMPY = 'numpy' + + +NEW_IMAGE_PATH_FLAG = '__new_image__' + + +TileOutputMimeTypes = { + 'JPEG': 'image/jpeg', + 'PNG': 'image/png', + 'TIFF': 'image/tiff', + # TILED indicates the region output should be generated as a tiled TIFF + 'TILED': 'image/tiff', + # JFIF forces conversion to JPEG through PIL to ensure the image is in a + # common colorspace. JPEG colorspace is complex: see + # https://docs.oracle.com/javase/8/docs/api/javax/imageio/metadata/ + # doc-files/jpeg_metadata.html + 'JFIF': 'image/jpeg', +} +TileOutputPILFormat = { + 'JFIF': 'JPEG', +} + +TileInputUnits = { + None: 'base_pixels', + 'base': 'base_pixels', + 'base_pixel': 'base_pixels', + 'base_pixels': 'base_pixels', + 'pixel': 'mag_pixels', + 'pixels': 'mag_pixels', + 'mag_pixel': 'mag_pixels', + 'mag_pixels': 'mag_pixels', + 'magnification_pixel': 'mag_pixels', + 'magnification_pixels': 'mag_pixels', + 'mm': 'mm', + 'millimeter': 'mm', + 'millimeters': 'mm', + 'fraction': 'fraction', + 'projection': 'projection', + 'proj': 'projection', + 'wgs84': 'proj4:EPSG:4326', + '4326': 'proj4:EPSG:4326', +} + +# numpy dtype to pyvips GValue +dtypeToGValue = { + 'b': 'char', + 'B': 'uchar', + 'd': 'double', + 'D': 'dpcomplex', + 'f': 'float', + 'F': 'complex', + 'h': 'short', + 'H': 'ushort', + 'i': 'int', + 'I': 'uint', +} +GValueToDtype = {v: k for k, v in dtypeToGValue.items()} +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/exceptions.html b/_modules/large_image/exceptions.html new file mode 100644 index 000000000..914050436 --- /dev/null +++ b/_modules/large_image/exceptions.html @@ -0,0 +1,181 @@ + + + + + + large_image.exceptions — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.exceptions

+import errno
+
+
+
+[docs] +class TileGeneralError(Exception): + pass
+ + + +
+[docs] +class TileSourceError(TileGeneralError): + pass
+ + + +
+[docs] +class TileSourceAssetstoreError(TileSourceError): + pass
+ + + +
+[docs] +class TileSourceXYZRangeError(TileSourceError): + pass
+ + + +
+[docs] +class TileSourceInefficientError(TileSourceError): + pass
+ + + +
+[docs] +class TileSourceFileNotFoundError(TileSourceError, FileNotFoundError): + def __init__(self, *args, **kwargs) -> None: + super().__init__(errno.ENOENT, *args, **kwargs)
+ + + +
+[docs] +class TileCacheError(TileGeneralError): + pass
+ + + +
+[docs] +class TileCacheConfigurationError(TileCacheError): + pass
+ + + +TileGeneralException = TileGeneralError +TileSourceException = TileSourceError +TileSourceAssetstoreException = TileSourceAssetstoreError +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource.html b/_modules/large_image/tilesource.html new file mode 100644 index 000000000..42f505844 --- /dev/null +++ b/_modules/large_image/tilesource.html @@ -0,0 +1,474 @@ + + + + + + large_image.tilesource — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource

+import os
+import re
+import sys
+import uuid
+from importlib.metadata import entry_points
+from pathlib import PosixPath
+from typing import Any, Dict, List, Optional, Tuple, Type, Union, cast
+
+from .. import config
+from ..constants import NEW_IMAGE_PATH_FLAG, SourcePriority
+from ..exceptions import (TileGeneralError, TileGeneralException,
+                          TileSourceAssetstoreError,
+                          TileSourceAssetstoreException, TileSourceError,
+                          TileSourceException, TileSourceFileNotFoundError)
+from .base import (TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY, TILE_FORMAT_PIL,
+                   FileTileSource, TileOutputMimeTypes, TileSource,
+                   dictToEtree, etreeToDict, nearPowerOfTwo)
+
+AvailableTileSources: Dict[str, Type[FileTileSource]] = {}
+
+
+def isGeospatial(
+        path: Union[str, PosixPath],
+        availableSources: Optional[Dict[str, Type[FileTileSource]]] = None) -> bool:
+    """
+    Check if a path is likely to be a geospatial file.
+
+    :param path: The path to the file
+    :param availableSources: an optional ordered dictionary of sources to use
+        for potentially checking the path.
+    :returns: True if geospatial.
+    """
+    if availableSources is None:
+        if not len(AvailableTileSources):
+            loadTileSources()
+        availableSources = AvailableTileSources
+    for sourceName in sorted(availableSources):
+        source = availableSources[sourceName]
+        if hasattr(source, 'isGeospatial'):
+            result = None
+            try:
+                result = source.isGeospatial(path)
+            except Exception:
+                pass
+            if result in (True, False):
+                return result
+    return False
+
+
+def loadTileSources(entryPointName: str = 'large_image.source',
+                    sourceDict: Dict[str, Type[FileTileSource]] = AvailableTileSources) -> None:
+    """
+    Load all tilesources from entrypoints and add them to the
+    AvailableTileSources dictionary.
+
+    :param entryPointName: the name of the entry points to load.
+    :param sourceDict: a dictionary to populate with the loaded sources.
+    """
+    epoints = entry_points()
+    # Python 3.10 uses select and deprecates dictionary interface
+    epointList = epoints.select(group=entryPointName) if hasattr(
+        epoints, 'select') else epoints.get(entryPointName, [])
+    for entryPoint in epointList:
+        try:
+            sourceClass = entryPoint.load()
+            if sourceClass.name and None in sourceClass.extensions:
+                sourceDict[entryPoint.name] = sourceClass
+                config.getLogger('logprint').debug('Loaded tile source %s' % entryPoint.name)
+        except Exception:
+            config.getLogger('logprint').exception(
+                'Failed to loaded tile source %s' % entryPoint.name)
+
+
+def getSortedSourceList(
+    availableSources: Dict[str, Type[FileTileSource]], pathOrUri: Union[str, PosixPath],
+    mimeType: Optional[str] = None, *args, **kwargs,
+) -> List[Tuple[bool, bool, SourcePriority, str]]:
+    """
+    Get an ordered list of sources where earlier sources are more likely to
+    work for a specified path or uri.
+
+    :param availableSources: an ordered dictionary of sources to try.
+    :param pathOrUri: either a file path or a fixed source via
+        large_image://<source>.
+    :param mimeType: the mimetype of the file, if known.
+    :returns: a list of (clash, fallback, priority, sourcename) for sources
+        where sourcename is a key in availableSources.
+    """
+    uriWithoutProtocol = str(pathOrUri).split('://', 1)[-1]
+    isLargeImageUri = str(pathOrUri).startswith('large_image://')
+    baseName = os.path.basename(uriWithoutProtocol)
+    extensions = [ext.lower() for ext in baseName.split('.')[1:]]
+    properties = {
+        '_geospatial_source': isGeospatial(pathOrUri, availableSources),
+    }
+    isNew = str(pathOrUri).startswith(NEW_IMAGE_PATH_FLAG)
+    ignored_names = config.getConfig('all_sources_ignored_names')
+    ignoreName = (ignored_names and re.search(
+        ignored_names, os.path.basename(str(pathOrUri)), flags=re.IGNORECASE))
+    sourceList = []
+    for sourceName in availableSources:
+        sourceExtensions = availableSources[sourceName].extensions
+        priority = sourceExtensions.get(None, SourcePriority.MANUAL)
+        fallback = True
+        if isNew and getattr(availableSources[sourceName], 'newPriority', None) is not None:
+            priority = min(priority, cast(SourcePriority, availableSources[sourceName].newPriority))
+        if (mimeType and getattr(availableSources[sourceName], 'mimeTypes', None) and
+                mimeType in availableSources[sourceName].mimeTypes):
+            priority = min(priority, availableSources[sourceName].mimeTypes[mimeType])
+            fallback = False
+        for regex in getattr(availableSources[sourceName], 'nameMatches', {}):
+            if re.match(regex, baseName):
+                priority = min(priority, availableSources[sourceName].nameMatches[regex])
+                fallback = False
+        for ext in extensions:
+            if ext in sourceExtensions:
+                priority = min(priority, sourceExtensions[ext])
+                fallback = False
+        if isLargeImageUri and sourceName == uriWithoutProtocol:
+            priority = SourcePriority.NAMED
+        if priority >= SourcePriority.MANUAL or (ignoreName and fallback):
+            continue
+        propertiesClash = any(
+            getattr(availableSources[sourceName], k, False) != v
+            for k, v in properties.items())
+        sourceList.append((propertiesClash, fallback, priority, sourceName))
+    return sourceList
+
+
+
+[docs] +def getSourceNameFromDict( + availableSources: Dict[str, Type[FileTileSource]], pathOrUri: Union[str, PosixPath], + mimeType: Optional[str] = None, *args, **kwargs) -> Optional[str]: + """ + Get a tile source based on a ordered dictionary of known sources and a path + name or URI. Additional parameters are passed to the tile source and can + be used for properties such as encoding. + + :param availableSources: an ordered dictionary of sources to try. + :param pathOrUri: either a file path or a fixed source via + large_image://<source>. + :param mimeType: the mimetype of the file, if known. + :returns: the name of a tile source that can read the input, or None if + there is no such source. + """ + sourceList = getSortedSourceList(availableSources, pathOrUri, mimeType, *args, **kwargs) + for entry in sorted(sourceList): + sourceName = entry[-1] + if availableSources[sourceName].canRead(pathOrUri, *args, **kwargs): + return sourceName + return None
+ + + +def getTileSourceFromDict( + availableSources: Dict[str, Type[FileTileSource]], pathOrUri: Union[str, PosixPath], + *args, **kwargs) -> FileTileSource: + """ + Get a tile source based on a ordered dictionary of known sources and a path + name or URI. Additional parameters are passed to the tile source and can + be used for properties such as encoding. + + :param availableSources: an ordered dictionary of sources to try. + :param pathOrUri: either a file path or a fixed source via + large_image://<source>. + :returns: a tile source instance or and error. + """ + sourceName = getSourceNameFromDict(availableSources, pathOrUri, *args, **kwargs) + if sourceName: + return availableSources[sourceName](pathOrUri, *args, **kwargs) + if not os.path.exists(pathOrUri) and '://' not in str(pathOrUri): + raise TileSourceFileNotFoundError(pathOrUri) + raise TileSourceError('No available tilesource for %s' % pathOrUri) + + +
+[docs] +def getTileSource(*args, **kwargs) -> FileTileSource: + """ + Get a tilesource using the known sources. If tile sources have not yet + been loaded, load them. + + :returns: A tilesource for the passed arguments. + """ + if not len(AvailableTileSources): + loadTileSources() + return getTileSourceFromDict(AvailableTileSources, *args, **kwargs)
+ + + +
+[docs] +def open(*args, **kwargs) -> FileTileSource: + """ + Alternate name of getTileSource. + + Get a tilesource using the known sources. If tile sources have not yet + been loaded, load them. + + :returns: A tilesource for the passed arguments. + """ + return getTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs) -> bool: + """ + Check if large_image can read a path or uri. + + If there is no intention to open the image immediately, conisder adding + `noCache=True` to the kwargs to avoid cycling the cache unnecessarily. + + :returns: True if any appropriate source reports it can read the path or + uri. + """ + if not len(AvailableTileSources): + loadTileSources() + if getSourceNameFromDict(AvailableTileSources, *args, **kwargs): + return True + return False
+ + + +def canReadList( + pathOrUri: Union[str, PosixPath], mimeType: Optional[str] = None, + availableSources: Optional[Dict[str, Type[FileTileSource]]] = None, + *args, **kwargs) -> List[Tuple[str, bool]]: + """ + Check if large_image can read a path or uri via each source. + + If there is no intention to open the image immediately, conisder adding + `noCache=True` to the kwargs to avoid cycling the cache unnecessarily. + + :param pathOrUri: either a file path or a fixed source via + large_image://<source>. + :param mimeType: the mimetype of the file, if known. + :param availableSources: an ordered dictionary of sources to try. If None, + use the primary list of sources. + :returns: A list of tuples of (source name, canRead). + """ + if availableSources is None and not len(AvailableTileSources): + loadTileSources() + sourceList = getSortedSourceList( + availableSources or AvailableTileSources, pathOrUri, mimeType, *args, **kwargs) + result = [] + for entry in sorted(sourceList): + sourceName = entry[-1] + result.append((sourceName, (availableSources or AvailableTileSources)[sourceName].canRead( + pathOrUri, *args, **kwargs))) + return result + + +
+[docs] +def new(*args, **kwargs) -> TileSource: + """ + Create a new image. + + TODO: add specific arguments to choose a source based on criteria. + """ + return getTileSource(NEW_IMAGE_PATH_FLAG + str(uuid.uuid4()), *args, **kwargs)
+ + + +
+[docs] +def listSources( + availableSources: Optional[Dict[str, Type[FileTileSource]]] = None, +) -> Dict[str, Dict[str, Any]]: + """ + Get a dictionary with all sources, all known extensions, and all known + mimetypes. + + :param availableSources: an ordered dictionary of sources to try. + :returns: a dictionary with sources, extensions, and mimeTypes. The + extensions and mimeTypes list their matching sources in priority order. + The sources list their supported extensions and mimeTypes with their + priority. + """ + if availableSources is None: + if not len(AvailableTileSources): + loadTileSources() + availableSources = AvailableTileSources + results: Dict[str, Dict[str, Any]] = {'sources': {}, 'extensions': {}, 'mimeTypes': {}} + for key, source in availableSources.items(): + if hasattr(source, 'addKnownExtensions'): + source.addKnownExtensions() + results['sources'][key] = { + 'extensions': { + k or 'default': v for k, v in source.extensions.items()}, + 'mimeTypes': { + k or 'default': v for k, v in source.mimeTypes.items()}, + } + for k, v in source.extensions.items(): + if k is not None: + results['extensions'].setdefault(k, []) + results['extensions'][k].append((v, key)) + results['extensions'][k].sort() + for k, v in source.mimeTypes.items(): + if k is not None: + results['mimeTypes'].setdefault(k, []) + results['mimeTypes'][k].append((v, key)) + results['mimeTypes'][k].sort() + for cls in source.__mro__: + try: + if sys.modules[cls.__module__].__version__: + results['sources'][key]['version'] = sys.modules[cls.__module__].__version__ + break + except Exception: + pass + return results
+ + + +
+[docs] +def listExtensions( + availableSources: Optional[Dict[str, Type[FileTileSource]]] = None) -> List[str]: + """ + Get a list of all known extensions. + + :param availableSources: an ordered dictionary of sources to try. + :returns: a list of extensions (without leading dots). + """ + return sorted(listSources(availableSources)['extensions'].keys())
+ + + +
+[docs] +def listMimeTypes( + availableSources: Optional[Dict[str, Type[FileTileSource]]] = None) -> List[str]: + """ + Get a list of all known mime types. + + :param availableSources: an ordered dictionary of sources to try. + :returns: a list of mime types. + """ + return sorted(listSources(availableSources)['mimeTypes'].keys())
+ + + +__all__ = [ + 'TileSource', 'FileTileSource', + 'exceptions', 'TileGeneralError', 'TileSourceError', + 'TileSourceAssetstoreError', 'TileSourceFileNotFoundError', + 'TileGeneralException', 'TileSourceException', 'TileSourceAssetstoreException', + 'TileOutputMimeTypes', 'TILE_FORMAT_IMAGE', 'TILE_FORMAT_PIL', 'TILE_FORMAT_NUMPY', + 'AvailableTileSources', 'getTileSource', 'getSourceNameFromDict', 'nearPowerOfTwo', + 'canRead', 'open', 'new', + 'listSources', 'listExtensions', 'listMimeTypes', + 'etreeToDict', 'dictToEtree', +] +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource/base.html b/_modules/large_image/tilesource/base.html new file mode 100644 index 000000000..4538ee9c2 --- /dev/null +++ b/_modules/large_image/tilesource/base.html @@ -0,0 +1,2727 @@ + + + + + + large_image.tilesource.base — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource.base

+import functools
+import io
+import json
+import math
+import os
+import pathlib
+import tempfile
+import threading
+import time
+import types
+import uuid
+from typing import Any, Dict, Iterator, List, Optional, Tuple, Union, cast
+
+import numpy as np
+import numpy.typing as npt
+import PIL
+import PIL.Image
+import PIL.ImageCms
+import PIL.ImageColor
+import PIL.ImageDraw
+
+from .. import config, exceptions
+from ..cache_util import getTileCache, methodcache, strhash
+from ..constants import (TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY, TILE_FORMAT_PIL,
+                         SourcePriority, TileInputUnits, TileOutputMimeTypes,
+                         TileOutputPILFormat)
+from . import utilities
+from .jupyter import IPyLeafletMixin
+from .tiledict import LazyTileDict
+from .tileiterator import TileIterator
+from .utilities import (ImageBytes, JSONDict, _imageToNumpy,  # noqa: F401
+                        _imageToPIL, dictToEtree, etreeToDict,
+                        getPaletteColors, histogramThreshold, nearPowerOfTwo)
+
+
+
+[docs] +class TileSource(IPyLeafletMixin): + # Name of the tile source + name = None + + # A dictionary of known file extensions and the ``SourcePriority`` given + # to each. It must contain a None key with a priority for the tile source + # when the extension does not match. + extensions: Dict[Optional[str], SourcePriority] = { + None: SourcePriority.FALLBACK, + } + + # A dictionary of common mime-types handled by the source and the + # ``SourcePriority`` given to each. This are used in place of or in + # additional to extensions. + mimeTypes: Dict[Optional[str], SourcePriority] = { + None: SourcePriority.FALLBACK, + } + + # A dictionary with regex strings as the keys and the ``SourcePriority`` + # given to names that match that expression. This is used in addition to + # extensions and mimeTypes, with the highest priority match taken. + nameMatches: Dict[str, SourcePriority] = { + } + + # If a source supports creating new tiled images, specify its basic + # priority based on expected feature set + newPriority: Optional[SourcePriority] = None + + # When getting tiles for otherwise empty levels (missing powers of two), we + # composite the tile from higher resolution levels. This can use excessive + # memory if there are too many missing levels. For instance, if there are + # six missing levels and the tile size is 1024 square RGBA, then 16 Gb are + # needed for the composited tile at a minimum. By setting + # _maxSkippedLevels, such large gaps are composited in stages. + _maxSkippedLevels = 3 + + _initValues: Tuple[Tuple[Any, ...], Dict[str, Any]] + _iccprofilesObjects: List[Any] + + def __init__(self, encoding: str = 'JPEG', jpegQuality: int = 95, + jpegSubsampling: int = 0, tiffCompression: str = 'raw', + edge: Union[bool, str] = False, + style: Optional[Union[str, Dict[str, int]]] = None, + noCache: Optional[bool] = None, + *args, **kwargs) -> None: + """ + Initialize the tile class. + + :param jpegQuality: when serving jpegs, use this quality. + :param jpegSubsampling: when serving jpegs, use this subsampling (0 is + full chroma, 1 is half, 2 is quarter). + :param encoding: 'JPEG', 'PNG', 'TIFF', or 'TILED'. + :param edge: False to leave edge tiles whole, True or 'crop' to crop + edge tiles, otherwise, an #rrggbb color to fill edges. + :param tiffCompression: the compression format to use when encoding a + TIFF. + :param style: if None, use the default style for the file. Otherwise, + this is a string with a json-encoded dictionary. The style can + contain the following keys: + + :band: if -1 or None, and if style is specified at all, the + greyscale value is used. Otherwise, a 1-based numerical + index into the channels of the image or a string that + matches the interpretation of the band ('red', 'green', + 'blue', 'gray', 'alpha'). Note that 'gray' on an RGB or + RGBA image will use the green band. + :frame: if specified, override the frame value for this band. + When used as part of a bands list, this can be used to + composite multiple frames together. It is most efficient + if at least one band either doesn't specify a frame + parameter or specifies the same frame value as the primary + query. + :framedelta: if specified and frame is not specified, override + the frame value for this band by using the current frame + plus this value. + :min: the value to map to the first palette value. Defaults to + 0. 'auto' to use 0 if the reported minimum and maximum of + the band are between [0, 255] or use the reported minimum + otherwise. 'min' or 'max' to always uses the reported + minimum or maximum. 'full' to always use 0. + :max: the value to map to the last palette value. Defaults to + 255. 'auto' to use 0 if the reported minimum and maximum + of the band are between [0, 255] or use the reported + maximum otherwise. 'min' or 'max' to always uses the + reported minimum or maximum. 'full' to use the maximum + value of the base data type (either 1, 255, or 65535). + :palette: a single color string, a palette name, or a list of + two or more color strings. Color strings are of the form + #RRGGBB, #RRGGBBAA, #RGB, #RGBA, or any string parseable by + the PIL modules, or, if it is installed, by matplotlib. A + single color string is the same as the list ['#000', + <color>]. Palette names are the name of a palettable + palette or, if available, a matplotlib palette. + :nodata: the value to use for missing data. null or unset to + not use a nodata value. + :composite: either 'lighten' or 'multiply'. Defaults to + 'lighten' for all except the alpha band. + :clamp: either True to clamp (also called clip or crop) values + outside of the [min, max] to the ends of the palette or + False to make outside values transparent. + :dtype: convert the results to the specified numpy dtype. + Normally, if a style is applied, the results are + intermediately a float numpy array with a value range of + [0,255]. If this is 'uint16', it will be cast to that and + multiplied by 65535/255. If 'float', it will be divided by + 255. If 'source', this uses the dtype of the source image. + :axis: keep only the specified axis from the numpy intermediate + results. This can be used to extract a single channel + after compositing. + + Alternately, the style object can contain a single key of 'bands', + which has a value which is a list of style dictionaries as above, + excepting that each must have a band that is not -1. Bands are + composited in the order listed. This base object may also contain + the 'dtype' and 'axis' values. + :param noCache: if True, the style can be adjusted dynamically and the + source is not elibible for caching. If there is no intention to + reuse the source at a later time, this can have performance + benefits, such as when first cataloging images that can be read. + """ + super().__init__(**kwargs) + self.logger = config.getLogger() + self.cache, self.cache_lock = getTileCache() + + self.tileWidth: int = 0 + self.tileHeight: int = 0 + self.levels: int = 0 + self.sizeX: int = 0 + self.sizeY: int = 0 + self._sourceLock = threading.RLock() + self._dtype: Optional[Union[npt.DTypeLike, str]] = None + self._bandCount: Optional[int] = None + + if encoding not in TileOutputMimeTypes: + raise ValueError('Invalid encoding "%s"' % encoding) + + self.encoding = encoding + self.jpegQuality = int(jpegQuality) + self.jpegSubsampling = int(jpegSubsampling) + self.tiffCompression = tiffCompression + self.edge = edge + self._setStyle(style) + + def __getstate__(self): + """ + Allow pickling. + + We reconstruct our state via the creation caused by the inverse of + reduce, so we don't report state here. + """ + return None + + def __reduce__(self) -> Tuple[functools.partial, Tuple[str]]: + """ + Allow pickling. + + Reduce can pass the args but not the kwargs, so use a partial class + call to reconstruct kwargs. + """ + import functools + import pickle + + if not hasattr(self, '_initValues') or hasattr(self, '_unpickleable'): + msg = 'Source cannot be pickled' + raise pickle.PicklingError(msg) + return functools.partial(type(self), **self._initValues[1]), self._initValues[0] + + def __repr__(self) -> str: + return self.getState() + + def _repr_png_(self): + return self.getThumbnail(encoding='PNG')[0] + + @property + def geospatial(self) -> bool: + return False + + def _setStyle(self, style: Any) -> None: + """ + Check and set the specified style from a json string or a dictionary. + + :param style: The new style. + """ + for key in {'_unlocked_classkey', '_classkeyLock'}: + try: + delattr(self, key) + except Exception: + pass + if not hasattr(self, '_bandRanges'): + self._bandRanges: Dict[Optional[int], Any] = {} + self._jsonstyle = style + if style is not None: + if isinstance(style, dict): + self._style: Optional[JSONDict] = JSONDict(style) + self._jsonstyle = json.dumps(style, sort_keys=True, separators=(',', ':')) + else: + try: + self._style = None + style = json.loads(style) + if not isinstance(style, dict): + raise TypeError + self._style = JSONDict(style) + except (TypeError, json.decoder.JSONDecodeError): + msg = 'Style is not a valid json object.' + raise exceptions.TileSourceError(msg) + +
+[docs] + def getBounds(self, *args, **kwargs) -> Dict[str, Any]: + return { + 'sizeX': self.sizeX, + 'sizeY': self.sizeY, + }
+ + +
+[docs] + def getCenter(self, *args, **kwargs) -> Tuple[float, float]: + """Returns (Y, X) center location.""" + if self.geospatial: + bounds = self.getBounds(*args, **kwargs) + return ( + (bounds['ymax'] - bounds['ymin']) / 2 + bounds['ymin'], + (bounds['xmax'] - bounds['xmin']) / 2 + bounds['xmin'], + ) + bounds = TileSource.getBounds(self, *args, **kwargs) + return (bounds['sizeY'] / 2, bounds['sizeX'] / 2)
+ + + @property + def style(self): + return self._style + + @style.setter + def style(self, value): + if not hasattr(self, '_unstyledStyle') and value == getattr(self, '_unstyledStyle', None): + return + if not getattr(self, '_noCache', False): + msg = 'Cannot set the style of a cached source' + raise exceptions.TileSourceError(msg) + args, kwargs = self._initValues + kwargs['style'] = value + self._initValues = (args, kwargs.copy()) + oldval = getattr(self, '_jsonstyle', None) + self._setStyle(value) + if oldval == getattr(self, '_jsonstyle', None): + return + self._classkey = str(uuid.uuid4()) + if (kwargs.get('style') != getattr(self, '_unstyledStyle', None) and + not hasattr(self, '_unstyledInstance')): + subkwargs = kwargs.copy() + subkwargs['style'] = getattr(self, '_unstyledStyle', None) + self._unstyledInstance = self.__class__(*args, **subkwargs) + + @property + def dtype(self) -> np.dtype: + with self._sourceLock: + if not self._dtype: + self._dtype = 'check' + sample, _ = cast(Tuple[np.ndarray, Any], getattr( + self, '_unstyledInstance', self).getRegion( + region=dict(left=0, top=0, width=1, height=1), + format=TILE_FORMAT_NUMPY)) + self._dtype = sample.dtype + self._bandCount = len( + getattr(getattr(self, '_unstyledInstance', self), '_bandInfo', [])) + if not self._bandCount: + self._bandCount = sample.shape[-1] if len(sample.shape) == 3 else 1 + return cast(np.dtype, self._dtype) + + @property + def bandCount(self) -> Optional[int]: + if not self._bandCount: + if not self._dtype or (isinstance(self._dtype, str) and self._dtype == 'check'): + return None + return self._bandCount + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs) -> str: + """ + Return a string hash used as a key in the recently-used cache for tile + sources. + + :returns: a string hash value. + """ + return strhash( + kwargs.get('encoding', 'JPEG'), kwargs.get('jpegQuality', 95), + kwargs.get('jpegSubsampling', 0), kwargs.get('tiffCompression', 'raw'), + kwargs.get('edge', False), + '__STYLESTART__', kwargs.get('style', None), '__STYLEEND__')
+ + +
+[docs] + def getState(self) -> str: + """ + Return a string reflecting the state of the tile source. This is used + as part of a cache key when hashing function return values. + + :returns: a string hash value of the source state. + """ + if hasattr(self, '_classkey'): + return self._classkey + return '%s,%s,%s,%s,%s,__STYLESTART__,%s,__STYLEEND__' % ( + self.encoding, + self.jpegQuality, + self.jpegSubsampling, + self.tiffCompression, + self.edge, + self._jsonstyle)
+ + +
+[docs] + def wrapKey(self, *args, **kwargs) -> str: + """ + Return a key for a tile source and function parameters that can be used + as a unique cache key. + + :param args: arguments to add to the hash. + :param kwaths: arguments to add to the hash. + :returns: a cache key. + """ + return strhash(self.getState()) + strhash(*args, **kwargs)
+ + + def _scaleFromUnits( + self, metadata: JSONDict, units: Optional[str], + desiredMagnification: Optional[Dict[str, Any]], + **kwargs) -> Tuple[float, float]: + """ + Get scaling parameters based on the source metadata and specified + units. + + :param metadata: the metadata associated with this source. + :param units: the units used for the scale. + :param desiredMagnification: the output from getMagnificationForLevel + for the desired magnification used to convert mag_pixels and mm. + :param kwargs: optional parameters. + :returns: (scaleX, scaleY) scaling parameters in the horizontal and + vertical directions. + """ + scaleX = scaleY = 1.0 + if units == 'fraction': + scaleX = metadata['sizeX'] + scaleY = metadata['sizeY'] + elif units == 'mag_pixels': + if not (desiredMagnification or {}).get('scale'): + msg = 'No magnification to use for units' + raise ValueError(msg) + scaleX = scaleY = cast(Dict[str, float], desiredMagnification)['scale'] + elif units == 'mm': + if (not (desiredMagnification or {}).get('scale') or + not (desiredMagnification or {}).get('mm_x') or + not (desiredMagnification or {}).get('mm_y')): + desiredMagnification = self.getNativeMagnification().copy() + cast(Dict[str, float], desiredMagnification)['scale'] = 1.0 + if (not desiredMagnification or + not desiredMagnification.get('scale') or + not desiredMagnification.get('mm_x') or + not desiredMagnification.get('mm_y')): + msg = 'No mm_x or mm_y to use for units' + raise ValueError(msg) + scaleX = (desiredMagnification['scale'] / + desiredMagnification['mm_x']) + scaleY = (desiredMagnification['scale'] / + desiredMagnification['mm_y']) + elif units in ('base_pixels', None): + pass + else: + raise ValueError('Invalid units %r' % units) + return scaleX, scaleY + + def _getRegionBounds( + self, + metadata: JSONDict, + left: Optional[float] = None, + top: Optional[float] = None, + right: Optional[float] = None, + bottom: Optional[float] = None, + width: Optional[float] = None, + height: Optional[float] = None, + units: Optional[str] = None, + desiredMagnification: Optional[Dict[str, Optional[float]]] = None, + cropToImage: bool = True, **kwargs) -> Tuple[float, float, float, float]: + """ + Given a set of arguments that can include left, right, top, bottom, + width, height, and units, generate actual pixel values for left, top, + right, and bottom. If left, top, right, or bottom are negative they + are interpreted as an offset from the right or bottom edge of the + image. + + :param metadata: the metadata associated with this source. + :param left: the left edge (inclusive) of the region to process. + :param top: the top edge (inclusive) of the region to process. + :param right: the right edge (exclusive) of the region to process. + :param bottom: the bottom edge (exclusive) of the region to process. + :param width: the width of the region to process. Ignored if both + left and right are specified. + :param height: the height of the region to process. Ignores if both + top and bottom are specified. + :param units: either 'base_pixels' (default), 'pixels', 'mm', or + 'fraction'. base_pixels are in maximum resolution pixels. + pixels is in the specified magnification pixels. mm is in the + specified magnification scale. fraction is a scale of 0 to 1. + pixels and mm are only available if the magnification and mm + per pixel are defined for the image. + :param desiredMagnification: the output from getMagnificationForLevel + for the desired magnification used to convert mag_pixels and mm. + :param cropToImage: if True, don't return region coordinates outside of + the image. + :param kwargs: optional parameters. These are passed to + _scaleFromUnits and may include unitsWH. + :returns: left, top, right, bottom bounds in pixels. + """ + if units not in TileInputUnits: + raise ValueError('Invalid units %r' % units) + # Convert units to max-resolution pixels + units = TileInputUnits[units] + scaleX, scaleY = self._scaleFromUnits(metadata, units, desiredMagnification, **kwargs) + if kwargs.get('unitsWH'): + if kwargs['unitsWH'] not in TileInputUnits: + raise ValueError('Invalid units %r' % kwargs['unitsWH']) + scaleW, scaleH = self._scaleFromUnits( + metadata, TileInputUnits[kwargs['unitsWH']], desiredMagnification, **kwargs) + # if unitsWH is specified, prefer width and height to right and + # bottom + if left is not None and right is not None and width is not None: + right = None + if top is not None and bottom is not None and height is not None: + bottom = None + else: + scaleW, scaleH = scaleX, scaleY + aggregion = {'left': left, 'top': top, 'right': right, + 'bottom': bottom, 'width': width, 'height': height} + region: Dict[str, float] = {key: cast(float, aggregion[key]) + for key in aggregion if aggregion[key] is not None} + for key, scale in ( + ('left', scaleX), ('right', scaleX), ('width', scaleW), + ('top', scaleY), ('bottom', scaleY), ('height', scaleH)): + if key in region and scale and scale != 1: + region[key] = region[key] * scale + # convert negative references to right or bottom offsets + for key in ('left', 'right', 'top', 'bottom'): + if key in region and region[key] < 0: + region[key] += metadata[ + 'sizeX' if key in ('left', 'right') else 'sizeY'] + # Calculate the region we need to fetch + left = region.get( + 'left', + (region['right'] - region['width']) + if 'right' in region and 'width' in region else 0) + right = region.get( + 'right', + (left + region['width']) + if 'width' in region else metadata['sizeX']) + top = region.get( + 'top', region['bottom'] - region['height'] + if 'bottom' in region and 'height' in region else 0) + bottom = region.get( + 'bottom', top + region['height'] + if 'height' in region else metadata['sizeY']) + if cropToImage: + # Crop the bounds to integer pixels within the actual source data + left = min(metadata['sizeX'], max(0, int(round(left)))) + right = min(metadata['sizeX'], max( + cast(int, left), int(round(cast(float, right))))) + top = min(metadata['sizeY'], max(0, int(round(top)))) + bottom = min(metadata['sizeY'], max( + cast(int, top), int(round(cast(float, bottom))))) + + return cast(int, left), cast(int, top), cast(int, right), cast(int, bottom) + + def _pilFormatMatches(self, image: Any, match: Union[bool, str] = True, **kwargs) -> bool: + """ + Determine if the specified PIL image matches the format of the tile + source with the specified arguments. + + :param image: the PIL image to check. + :param match: if 'any', all image encodings are considered matching, + if 'encoding', then a matching encoding matches regardless of + quality options, otherwise, only match if the encoding and quality + options match. + :param kwargs: additional parameters to use in determining format. + """ + encoding = TileOutputPILFormat.get(self.encoding, self.encoding) + if match == 'any' and encoding in ('PNG', 'JPEG'): + return True + if image.format != encoding: + return False + if encoding == 'PNG': + return True + if encoding == 'JPEG': + if match == 'encoding': + return True + originalQuality = None + try: + if image.format == 'JPEG' and hasattr(image, 'quantization'): + if image.quantization[0][58] <= 100: + originalQuality = int(100 - image.quantization[0][58] / 2) + else: + originalQuality = int(5000.0 / 2.5 / image.quantization[0][15]) + except Exception: + return False + return bool(originalQuality and abs(originalQuality - self.jpegQuality) <= 1) + # We fail for the TIFF file format; it is general enough that ensuring + # compatibility could be an issue. + return False + +
+[docs] + @methodcache() + def histogram( # noqa + self, dtype: npt.DTypeLike = None, onlyMinMax: bool = False, + bins: int = 256, density: bool = False, format: Any = None, + *args, **kwargs) -> Dict[str, Union[np.ndarray, List[Dict[str, Any]]]]: + """ + Get a histogram for a region. + + :param dtype: if specified, the tiles must be this numpy.dtype. + :param onlyMinMax: if True, only return the minimum and maximum value + of the region. + :param bins: the number of bins in the histogram. This is passed to + numpy.histogram, but needs to produce the same set of edges for + each tile. + :param density: if True, scale the results based on the number of + samples. + :param format: ignored. Used to override the format for the + tileIterator. + :param range: if None, use the computed min and (max + 1). Otherwise, + this is the range passed to numpy.histogram. Note this is only + accessible via kwargs as it otherwise overloads the range function. + If 'round', use the computed values, but the number of bins may be + reduced or the bin_edges rounded to integer values for + integer-based source data. + :param args: parameters to pass to the tileIterator. + :param kwargs: parameters to pass to the tileIterator. + :returns: if onlyMinMax is true, this is a dictionary with keys min and + max, each of which is a numpy array with the minimum and maximum of + all of the bands. If onlyMinMax is False, this is a dictionary + with a single key 'histogram' that contains a list of histograms + per band. Each entry is a dictionary with min, max, range, hist, + bins, and bin_edges. range is [min, (max + 1)]. hist is the + counts (normalized if density is True) for each bin. bins is the + number of bins used. bin_edges is an array one longer than the + hist array that contains the boundaries between bins. + """ + lastlog = time.time() + kwargs = kwargs.copy() + histRange = kwargs.pop('range', None) + results: Optional[Dict[str, Any]] = None + for itile in self.tileIterator(format=TILE_FORMAT_NUMPY, **kwargs): + if time.time() - lastlog > 10: + self.logger.info( + 'Calculating histogram min/max %d/%d', + itile['tile_position']['position'], itile['iterator_range']['position']) + lastlog = time.time() + tile = itile['tile'] + if dtype is not None and tile.dtype != dtype: + if tile.dtype == np.uint8 and dtype == np.uint16: + tile = np.array(tile, dtype=np.uint16) * 257 + else: + continue + tilemin = np.array([ + np.amin(tile[:, :, idx]) for idx in range(tile.shape[2])], tile.dtype) + tilemax = np.array([ + np.amax(tile[:, :, idx]) for idx in range(tile.shape[2])], tile.dtype) + tilesum: np.ndarray = np.array([ + np.sum(tile[:, :, idx]) for idx in range(tile.shape[2])], float) + tilesum2: np.ndarray = np.array([ + np.sum(np.array(tile[:, :, idx], float) ** 2) + for idx in range(tile.shape[2])], float) + tilecount = tile.shape[0] * tile.shape[1] + if results is None: + results = { + 'min': tilemin, + 'max': tilemax, + 'sum': tilesum, + 'sum2': tilesum2, + 'count': tilecount, + } + else: + results['min'] = np.minimum(results['min'], tilemin[:len(results['min'])]) + results['max'] = np.maximum(results['max'], tilemax[:len(results['min'])]) + results['sum'] += tilesum[:len(results['min'])] + results['sum2'] += tilesum2[:len(results['min'])] + results['count'] += tilecount + if results is None: + return {} + results['mean'] = results['sum'] / results['count'] + results['stdev'] = np.maximum( + results['sum2'] / results['count'] - results['mean'] ** 2, + [0] * results['sum2'].shape[0]) ** 0.5 + results.pop('sum', None) + results.pop('sum2', None) + results.pop('count', None) + if results is None or onlyMinMax: + return results + results['histogram'] = [{ + 'min': results['min'][idx], + 'max': results['max'][idx], + 'mean': results['mean'][idx], + 'stdev': results['stdev'][idx], + 'range': ((results['min'][idx], results['max'][idx] + 1) + if histRange is None or histRange == 'round' else histRange), + 'hist': None, + 'bin_edges': None, + 'bins': bins, + 'density': bool(density), + } for idx in range(len(results['min']))] + if histRange == 'round' and np.issubdtype(dtype or self.dtype, np.integer): + for record in results['histogram']: + if (record['range'][1] - record['range'][0]) < bins * 10: + step = int(math.ceil((record['range'][1] - record['range'][0]) / bins)) + rbins = int(math.ceil((record['range'][1] - record['range'][0]) / step)) + record['range'] = (record['range'][0], record['range'][0] + step * rbins) + record['bins'] = rbins + for tile in self.tileIterator(format=TILE_FORMAT_NUMPY, **kwargs): + if time.time() - lastlog > 10: + self.logger.info( + 'Calculating histogram %d/%d', + tile['tile_position']['position'], tile['iterator_range']['position']) + lastlog = time.time() + tile = tile['tile'] + if dtype is not None and tile.dtype != dtype: + if tile.dtype == np.uint8 and dtype == np.uint16: + tile = np.array(tile, dtype=np.uint16) * 257 + else: + continue + for idx in range(len(results['min'])): + entry = results['histogram'][idx] + hist, bin_edges = np.histogram( + tile[:, :, idx], entry['bins'], entry['range'], density=False) + if entry['hist'] is None: + entry['hist'] = hist + entry['bin_edges'] = bin_edges + else: + entry['hist'] += hist + for idx in range(len(results['min'])): + entry = results['histogram'][idx] + if entry['hist'] is not None: + entry['samples'] = np.sum(entry['hist']) + if density: + entry['hist'] = entry['hist'].astype(float) / entry['samples'] + return results
+ + + def _scanForMinMax( + self, dtype: npt.DTypeLike, frame: Optional[int] = None, + analysisSize: int = 1024, onlyMinMax: bool = True, **kwargs) -> None: + """ + Scan the image at a lower resolution to find the minimum and maximum + values. The results are stored in an internal dictionary. + + :param dtype: the numpy dtype. Used for guessing the range. + :param frame: the frame to use for auto-ranging. + :param analysisSize: the size of the image to use for analysis. + :param onlyMinMax: if True, only find the min and max. If False, get + the entire histogram. + """ + self._bandRanges[frame] = getattr(self, '_unstyledInstance', self).histogram( + dtype=dtype, + onlyMinMax=onlyMinMax, + output={'maxWidth': min(self.sizeX, analysisSize), + 'maxHeight': min(self.sizeY, analysisSize)}, + resample=False, + frame=frame, **kwargs) + if self._bandRanges[frame]: + self.logger.info('Style range is %r', { + k: v for k, v in self._bandRanges[frame].items() if k in { + 'min', 'max', 'mean', 'stdev'}}) + + def _validateMinMaxValue( + self, value: Union[str, float], frame: int, dtype: npt.DTypeLike, + ) -> Tuple[Union[str, int, float], Union[float, int]]: + """ + Validate the min/max setting and return a specific string or float + value and with any threshold. + + :param value: the specified value, 'auto', 'min', or 'max'. 'auto' + uses the parameter specified in 'minmax' or 0 or 255 if the + band's minimum is in the range [0, 254] and maximum is in the range + [2, 255]. 'min:<value>' and 'max:<value>' use the histogram to + threshold the image based on the value. 'auto:<value>' applies a + histogram threshold if the parameter specified in minmax is used. + :param dtype: the numpy dtype. Used for guessing the range. + :param frame: the frame to use for auto-ranging. + :returns: the validated value and a threshold from [0-1]. + """ + threshold: float = 0 + if value not in {'min', 'max', 'auto', 'full'}: + try: + if ':' in str(value) and cast(str, value).split(':', 1)[0] in { + 'min', 'max', 'auto'}: + threshold = float(cast(str, value).split(':', 1)[1]) + value = cast(str, value).split(':', 1)[0] + else: + value = float(value) + except ValueError: + self.logger.warning('Style min/max value of %r is not valid; using "auto"', value) + value = 'auto' + if value in {'min', 'max', 'auto'} and ( + frame not in self._bandRanges or ( + threshold and 'histogram' not in self._bandRanges[frame])): + self._scanForMinMax(dtype, frame, onlyMinMax=not threshold) + return value, threshold + + def _getMinMax( # noqa + self, minmax: str, value: Union[str, float], dtype: np.dtype, + bandidx: Optional[int] = None, frame: Optional[int] = None) -> float: + """ + Get an appropriate minimum or maximum for a band. + + :param minmax: either 'min' or 'max'. + :param value: the specified value, 'auto', 'min', or 'max'. 'auto' + uses the parameter specified in 'minmax' or 0 or 255 if the + band's minimum is in the range [0, 254] and maximum is in the range + [2, 255]. 'min:<value>' and 'max:<value>' use the histogram to + threshold the image based on the value. 'auto:<value>' applies a + histogram threshold if the parameter specified in minmax is used. + :param dtype: the numpy dtype. Used for guessing the range. + :param bandidx: the index of the channel that could be used for + determining the min or max. + :param frame: the frame to use for auto-ranging. + """ + frame = frame or 0 + value, threshold = self._validateMinMaxValue(value, frame, dtype) + if value == 'full': + value = 0 + if minmax != 'min': + if dtype == np.uint16: + value = 65535 + elif dtype.kind == 'f': + value = 1 + else: + value = 255 + if value == 'auto': + if (self._bandRanges.get(frame) and + np.all(self._bandRanges[frame]['min'] >= 0) and + np.all(self._bandRanges[frame]['min'] <= 254) and + np.all(self._bandRanges[frame]['max'] >= 2) and + np.all(self._bandRanges[frame]['max'] <= 255)): + value = 0 if minmax == 'min' else 255 + else: + value = minmax + if value == 'min': + if bandidx is not None and self._bandRanges.get(frame): + if threshold: + value = histogramThreshold( + self._bandRanges[frame]['histogram'][bandidx], threshold) + else: + value = self._bandRanges[frame]['min'][bandidx] + else: + value = 0 + elif value == 'max': + if bandidx is not None and self._bandRanges.get(frame): + if threshold: + value = histogramThreshold( + self._bandRanges[frame]['histogram'][bandidx], threshold, True) + else: + value = self._bandRanges[frame]['max'][bandidx] + elif dtype == np.uint16: + value = 65535 + elif dtype.kind == 'f': + value = 1 + else: + value = 255 + return float(value) + + def _applyStyleFunction( + self, image: np.ndarray, sc: types.SimpleNamespace, stage: str, + function: Optional[Dict[str, Any]] = None) -> np.ndarray: + """ + Check if a style ahs a style function for the current stage. If so, + apply it. + + :param image: the numpy image to adjust. This varies by stage: + For pre, this is the source image. + For preband, this is the band image (often the source image). + For band, this is the scaled band image before palette has been + applied. + For postband, this is the output image at the current time. + For main, this is the output image before adjusting to the target + style. + For post, this is the final output image. + :param sc: the style context. + :param stage: one of the stages: pre, preband, band, postband, main, + post. + :param function: if None, this is taken from the sc.style object using + the appropriate band index. Otherwise, this is a style: either a + list of style objects, or a style object with name (the + module.function_name), stage (either a stage or a list of stages + that this function applies to), context (falsy to not pass the + style context to the function, True to pass it as the parameter + 'context', or a string to pass it as a parameter of that name), + parameters (a dictionary of parameters to pass to the function). + If function is a string, it is shorthand for {'name': <function>}. + :returns: the modified numpy image. + """ + import importlib + + if function is None: + function = ( + sc.style.get('function') if not hasattr(sc, 'styleIndex') else + sc.style['bands'][sc.styleIndex].get('function')) + if function is None: + return image + if isinstance(function, (list, tuple)): + for func in cast(Union[List, Tuple], function): + image = self._applyStyleFunction(image, sc, stage, func) + return image + if isinstance(function, str): + function = {'name': function} + useOnStages = ( + [function['stage']] if isinstance(function.get('stage'), str) + else function.get('stage', ['main', 'band'])) + if stage not in useOnStages: + return image + sc.stage = stage + try: + module_name, func_name = function['name'].rsplit('.', 1) + module = importlib.import_module(module_name) + func = getattr(module, func_name) + except Exception as exc: + self._styleFunctionWarnings = getattr(self, '_styleFunctionWarnings', {}) + if function['name'] not in self._styleFunctionWarnings: + self._styleFunctionWarnings[function['name']] = exc + self.logger.exception('Failed to import style function %s', function['name']) + return image + kwargs = function.get('parameters', {}).copy() + if function.get('context'): + kwargs['context' if function['context'] is True else function['context']] = sc + try: + return func(image, **kwargs) + except Exception as exc: + self._styleFunctionWarnings = getattr(self, '_styleFunctionWarnings', {}) + if function['name'] not in self._styleFunctionWarnings: + self._styleFunctionWarnings[function['name']] = exc + self.logger.exception('Failed to execute style function %s', function['name']) + return image + +
+[docs] + def getICCProfiles( + self, idx: Optional[int] = None, onlyInfo: bool = False) -> Optional[ + Union[PIL.ImageCms.ImageCmsProfile, List[Optional[PIL.ImageCms.ImageCmsProfile]]]]: + """ + Get a list of all ICC profiles that are available for the source, or + get a specific profile. + + :param idx: a 0-based index into the profiles to get one profile, or + None to get a list of all profiles. + :param onlyInfo: if idx is None and this is true, just return the + profile information. + :returns: either one or a list of PIL.ImageCms.CmsProfile objects, or + None if no profiles are available. If a list, entries in the list + may be None. + """ + if not hasattr(self, '_iccprofiles'): + return None + results = [] + for pidx, prof in enumerate(self._iccprofiles): + if idx is not None and pidx != idx: + continue + if hasattr(self, '_iccprofilesObjects') and self._iccprofilesObjects[pidx] is not None: + prof = self._iccprofilesObjects[pidx]['profile'] + elif not isinstance(prof, PIL.ImageCms.ImageCmsProfile): + try: + prof = PIL.ImageCms.getOpenProfile(io.BytesIO(prof)) + except PIL.ImageCms.PyCMSError: + continue + if idx == pidx: + return prof + results.append(prof) + if onlyInfo: + results = [ + PIL.ImageCms.getProfileInfo(prof).strip() or 'present' + if prof else None for prof in results] + return results
+ + + def _applyICCProfile(self, sc: types.SimpleNamespace, frame: int) -> np.ndarray: + """ + Apply an ICC profile to an image. + + :param sc: the style context. + :param frame: the frame to use for auto ranging. + :returns: an image with the icc profile, if any, applied. + """ + if not hasattr(self, '_iccprofiles'): + return sc.image + profileIdx = frame if frame and len(self._iccprofiles) >= frame + 1 else 0 + sc.iccimage = sc.image + sc.iccapplied = False + if not self._iccprofiles[profileIdx]: + return sc.image + if not hasattr(self, '_iccprofilesObjects'): + self._iccprofilesObjects = [None] * len(self._iccprofiles) + image = _imageToPIL(sc.image) + mode = image.mode + if hasattr(PIL.ImageCms, 'Intent'): # PIL >= 9 + intent = getattr(PIL.ImageCms.Intent, str(sc.style.get('icc')).upper(), + PIL.ImageCms.Intent.PERCEPTUAL) + else: + intent = getattr(PIL.ImageCms, 'INTENT_' + str(sc.style.get('icc')).upper(), + PIL.ImageCms.INTENT_PERCEPTUAL) # type: ignore[attr-defined] + if not hasattr(self, '_iccsrgbprofile'): + try: + self._iccsrgbprofile = PIL.ImageCms.createProfile('sRGB') + except ImportError: + self._iccsrgbprofile = None + self.logger.warning( + 'Failed to import PIL.ImageCms. Cannot perform ICC ' + 'color adjustments. Does your platform support ' + 'PIL.ImageCms?') + if self._iccsrgbprofile is None: + return sc.image + try: + key = (mode, intent) + if self._iccprofilesObjects[profileIdx] is None: + self._iccprofilesObjects[profileIdx] = { + 'profile': self.getICCProfiles(profileIdx), + } + if key not in self._iccprofilesObjects[profileIdx]: + self._iccprofilesObjects[profileIdx][key] = \ + PIL.ImageCms.buildTransformFromOpenProfiles( + self._iccprofilesObjects[profileIdx]['profile'], + self._iccsrgbprofile, mode, mode, + renderingIntent=intent) + self.logger.debug( + 'Created an ICC profile transform for mode %s, intent %s', mode, intent) + transform = self._iccprofilesObjects[profileIdx][key] + + PIL.ImageCms.applyTransform(image, transform, inPlace=True) + sc.iccimage = _imageToNumpy(image)[0] + sc.iccapplied = True + except Exception as exc: + if not hasattr(self, '_iccerror'): + self._iccerror = exc + self.logger.exception('Failed to apply ICC profile') + return sc.iccimage + + def _applyStyle( # noqa + self, image: np.ndarray, style: Optional[JSONDict], x: int, y: int, + z: int, frame: Optional[int] = None) -> np.ndarray: + """ + Apply a style to a numpy image. + + :param image: the image to modify. + :param style: a style object. + :param x: the x tile position; used for multi-frame styles. + :param y: the y tile position; used for multi-frame styles. + :param z: the z tile position; used for multi-frame styles. + :param frame: the frame to use for auto ranging. + :returns: a styled image. + """ + sc = types.SimpleNamespace( + image=image, originalStyle=style, x=x, y=y, z=z, frame=frame, + mainImage=image, mainFrame=frame, dtype=None, axis=None) + if not style or ('icc' in style and len(style) == 1): + sc.style = {'icc': (style or cast(JSONDict, {})).get( + 'icc', config.getConfig('icc_correction', True)), 'bands': []} + else: + sc.style = style if 'bands' in style else {'bands': [style]} + sc.dtype = style.get('dtype') + sc.axis = style.get('axis') + if hasattr(self, '_iccprofiles') and sc.style.get( + 'icc', config.getConfig('icc_correction', True)): + image = self._applyICCProfile(sc, frame or 0) + if not style or ('icc' in style and len(style) == 1): + sc.output = image + else: + newwidth = 4 + if (len(sc.style['bands']) == 1 and sc.style['bands'][0].get('band') != 'alpha' and + image.shape[-1] == 1): + palette = getPaletteColors(sc.style['bands'][0].get('palette', ['#000', '#FFF'])) + if np.array_equal(palette, getPaletteColors('#fff')): + newwidth = 1 + sc.output = np.zeros( + (image.shape[0], image.shape[1], newwidth), + np.float32 if image.dtype != np.float64 else image.dtype) + image = self._applyStyleFunction(image, sc, 'pre') + for eidx, entry in enumerate(sc.style['bands']): + sc.styleIndex = eidx + sc.dtype = sc.dtype if sc.dtype is not None else entry.get('dtype') + if sc.dtype == 'source': + if sc.mainImage.dtype == np.uint16: + sc.dtype = 'uint16' + elif sc.mainImage.dtype.kind == 'f': + sc.dtype = 'float' + sc.axis = sc.axis if sc.axis is not None else entry.get('axis') + sc.bandidx = 0 if image.shape[2] <= 2 else 1 # type: ignore[misc] + sc.band = None + if ((entry.get('frame') is None and not entry.get('framedelta')) or + entry.get('frame') == sc.mainFrame): + image = sc.mainImage + frame = sc.mainFrame + else: + frame = entry['frame'] if entry.get('frame') is not None else ( + sc.mainFrame + entry['framedelta']) + image = getattr(self, '_unstyledInstance', self).getTile( + x, y, z, frame=frame, numpyAllowed=True) + image = image[:sc.mainImage.shape[0], + :sc.mainImage.shape[1], + :sc.mainImage.shape[2]] + if (isinstance(entry.get('band'), int) and + entry['band'] >= 1 and entry['band'] <= image.shape[2]): # type: ignore[misc] + sc.bandidx = entry['band'] - 1 + sc.composite = entry.get('composite', 'lighten') + if (hasattr(self, '_bandnames') and entry.get('band') and + str(entry['band']).lower() in self._bandnames and + image.shape[2] > self._bandnames[ # type: ignore[misc] + str(entry['band']).lower()]): + sc.bandidx = self._bandnames[str(entry['band']).lower()] + if entry.get('band') == 'red' and image.shape[2] > 2: # type: ignore[misc] + sc.bandidx = 0 + elif entry.get('band') == 'blue' and image.shape[2] > 2: # type: ignore[misc] + sc.bandidx = 2 + sc.band = image[:, :, 2] + elif entry.get('band') == 'alpha': + sc.bandidx = (image.shape[2] - 1 if image.shape[2] in (2, 4) # type: ignore[misc] + else None) + sc.band = (image[:, :, -1] if image.shape[2] in (2, 4) else # type: ignore[misc] + np.full(image.shape[:2], 255, np.uint8)) + sc.composite = entry.get('composite', 'multiply') + if sc.band is None: + sc.band = image[ + :, :, sc.bandidx # type: ignore[index] + if sc.bandidx is not None and sc.bandidx < image.shape[2] # type: ignore[misc] + else 0] + sc.band = self._applyStyleFunction(sc.band, sc, 'preband') + sc.palette = getPaletteColors(entry.get( + 'palette', ['#000', '#FFF'] + if entry.get('band') != 'alpha' else ['#FFF0', '#FFFF'])) + sc.discrete = entry.get('scheme') == 'discrete' + sc.palettebase = np.linspace(0, 1, len(sc.palette), endpoint=True) + sc.nodata = entry.get('nodata') + sc.min = self._getMinMax( + 'min', entry.get('min', 'auto'), image.dtype, sc.bandidx, frame) + sc.max = self._getMinMax( + 'max', entry.get('max', 'auto'), image.dtype, sc.bandidx, frame) + sc.clamp = entry.get('clamp', True) + delta = sc.max - sc.min if sc.max != sc.min else 1 + if sc.nodata is not None: + sc.mask = sc.band != float(sc.nodata) + else: + sc.mask = np.full(image.shape[:2], True) + sc.band = (sc.band - sc.min) / delta + if not sc.clamp: + sc.mask = sc.mask & (sc.band >= 0) & (sc.band <= 1) + sc.band = self._applyStyleFunction(sc.band, sc, 'band') + # To implement anything other multiply or lighten, we should mimic + # mapnik (and probably delegate to a family of functions). + # mapnik's options are: clear src dst src_over dst_over src_in + # dst_in src_out dst_out src_atop dst_atop xor plus minus multiply + # screen overlay darken lighten color_dodge color_burn hard_light + # soft_light difference exclusion contrast invert grain_merge + # grain_extract hue saturation color value linear_dodge linear_burn + # divide. + # See https://docs.gimp.org/en/gimp-concepts-layer-modes.html for + # some details. + for channel in range(sc.output.shape[2]): # type: ignore[misc] + if np.all(sc.palette[:, channel] == sc.palette[0, channel]): + if ((sc.palette[0, channel] == 0 and sc.composite != 'multiply') or + (sc.palette[0, channel] == 255 and sc.composite == 'multiply')): + continue + clrs = np.full(sc.band.shape, sc.palette[0, channel], dtype=sc.band.dtype) + else: + # Don't recompute if the sc.palette is repeated two channels + # in a row. + if not channel or np.any( + sc.palette[:, channel] != sc.palette[:, channel - 1]): + if not sc.discrete: + clrs = np.interp(sc.band, sc.palettebase, sc.palette[:, channel]) + else: + clrs = sc.palette[ + np.floor(sc.band * len(sc.palette)).astype(int).clip( + 0, len(sc.palette) - 1), channel] + if sc.composite == 'multiply': + if eidx: + sc.output[:sc.mask.shape[0], :sc.mask.shape[1], channel] = np.multiply( + sc.output[:sc.mask.shape[0], :sc.mask.shape[1], channel], + np.where(sc.mask, clrs / 255, 1)) + else: + if not eidx: + sc.output[:sc.mask.shape[0], + :sc.mask.shape[1], + channel] = np.where(sc.mask, clrs, 0) + else: + sc.output[:sc.mask.shape[0], :sc.mask.shape[1], channel] = np.maximum( + sc.output[:sc.mask.shape[0], :sc.mask.shape[1], channel], + np.where(sc.mask, clrs, 0)) + sc.output = self._applyStyleFunction(sc.output, sc, 'postband') + if hasattr(sc, 'styleIndex'): + del sc.styleIndex + sc.output = self._applyStyleFunction(sc.output, sc, 'main') + if sc.dtype == 'uint8': + sc.output = sc.output.astype(np.uint8) + elif sc.dtype == 'uint16': + sc.output = (sc.output * 65535 / 255).astype(np.uint16) + elif sc.dtype == 'float': + sc.output /= 255 + if sc.axis is not None and 0 <= int(sc.axis) < sc.output.shape[2]: # type: ignore[misc] + sc.output = sc.output[:, :, sc.axis:sc.axis + 1] + sc.output = self._applyStyleFunction(sc.output, sc, 'post') + return sc.output + + def _outputTileNumpyStyle( + self, intile: Any, applyStyle: bool, x: int, y: int, z: int, + frame: Optional[int] = None) -> Tuple[np.ndarray, str]: + """ + Convert a tile to a numpy array. Optionally apply the style to a tile. + Always returns a numpy tile. + + :param tile: the tile to convert. + :param applyStyle: if True and there is a style, apply it. + :param x: the x tile position; used for multi-frame styles. + :param y: the y tile position; used for multi-frame styles. + :param z: the z tile position; used for multi-frame styles. + :param frame: the frame to use for auto-ranging. + :returns: a numpy array and a target PIL image mode. + """ + tile, mode = _imageToNumpy(intile) + if (applyStyle and (getattr(self, 'style', None) or hasattr(self, '_iccprofiles')) and + (not getattr(self, 'style', None) or len(self.style) != 1 or + self.style.get('icc') is not False)): + tile = self._applyStyle(tile, getattr(self, 'style', None), x, y, z, frame) + if tile.shape[0] != self.tileHeight or tile.shape[1] != self.tileWidth: + extend = np.zeros( + (self.tileHeight, self.tileWidth, tile.shape[2]), # type: ignore[misc] + dtype=tile.dtype) + extend[:min(self.tileHeight, tile.shape[0]), + :min(self.tileWidth, tile.shape[1])] = tile + tile = extend + return tile, mode + + def _outputTile( + self, tile: Union[ImageBytes, PIL.Image.Image, bytes, np.ndarray], + tileEncoding: str, x: int, y: int, z: int, + pilImageAllowed: bool = False, + numpyAllowed: Union[bool, str] = False, applyStyle: bool = True, + **kwargs) -> Union[ImageBytes, PIL.Image.Image, bytes, np.ndarray]: + """ + Convert a tile from a numpy array, PIL image, or image in memory to the + desired encoding. + + :param tile: the tile to convert. + :param tileEncoding: the current tile encoding. + :param x: tile x value. Used for cropping or edge adjustment. + :param y: tile y value. Used for cropping or edge adjustment. + :param z: tile z (level) value. Used for cropping or edge adjustment. + :param pilImageAllowed: True if a PIL image may be returned. + :param numpyAllowed: True if a numpy image may be returned. 'always' + to return a numpy array. + :param applyStyle: if True and there is a style, apply it. + :returns: either a numpy array, a PIL image, or a memory object with an + image file. + """ + isEdge = False + if self.edge: + sizeX = int(self.sizeX * 2 ** (z - (self.levels - 1))) + sizeY = int(self.sizeY * 2 ** (z - (self.levels - 1))) + maxX = (x + 1) * self.tileWidth + maxY = (y + 1) * self.tileHeight + isEdge = maxX > sizeX or maxY > sizeY + hasStyle = ( + len(set(getattr(self, 'style', {})) - {'icc'}) or + getattr(self, 'style', {}).get('icc', config.getConfig('icc_correction', True))) + if (tileEncoding not in (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY) and + numpyAllowed != 'always' and tileEncoding == self.encoding and + not isEdge and (not applyStyle or not hasStyle)): + return tile + + if self._dtype is None or (isinstance(self._dtype, str) and self._dtype == 'check'): + if isinstance(tile, np.ndarray): + self._dtype = tile.dtype + self._bandCount = tile.shape[-1] if len(tile.shape) == 3 else 1 + elif isinstance(tile, PIL.Image.Image): + self._dtype = np.uint8 if ';16' not in tile.mode else np.uint16 + self._bandCount = len(tile.mode) + else: + _img = _imageToNumpy(tile)[0] + self._dtype = _img.dtype + self._bandCount = _img.shape[-1] if len(_img.shape) == 3 else 1 + + mode = None + if (numpyAllowed == 'always' or tileEncoding == TILE_FORMAT_NUMPY or + (applyStyle and hasStyle) or isEdge): + tile, mode = self._outputTileNumpyStyle( + tile, applyStyle, x, y, z, self._getFrame(**kwargs)) + if isEdge: + contentWidth = min(self.tileWidth, + sizeX - (maxX - self.tileWidth)) + contentHeight = min(self.tileHeight, + sizeY - (maxY - self.tileHeight)) + tile, mode = _imageToNumpy(tile) + if self.edge in (True, 'crop'): + tile = tile[:contentHeight, :contentWidth] + else: + color = PIL.ImageColor.getcolor(self.edge, mode) + tile = tile.copy() + tile[:, contentWidth:] = color + tile[contentHeight:] = color + if isinstance(tile, np.ndarray) and numpyAllowed: + return tile + tile = _imageToPIL(tile) + if pilImageAllowed: + return tile + # If we can't redirect, but the tile is read from a file in the desired + # output format, just read the file + if getattr(tile, 'fp', None) and self._pilFormatMatches(tile): + tile.fp.seek(0) # type: ignore + return tile.fp.read() # type: ignore + result = utilities._encodeImageBinary( + tile, self.encoding, self.jpegQuality, self.jpegSubsampling, self.tiffCompression) + return result + + def _getAssociatedImage(self, imageKey: str) -> Optional[PIL.Image.Image]: + """ + Get an associated image in PIL format. + + :param imageKey: the key of the associated image. + :return: the image in PIL format or None. + """ + return None + +
+[docs] + @classmethod + def canRead(cls, *args, **kwargs): + """ + Check if we can read the input. This takes the same parameters as + __init__. + + :returns: True if this class can read the input. False if it cannot. + """ + return False
+ + +
+[docs] + def getMetadata(self) -> JSONDict: + """ + Return metadata about this tile source. This contains + + :levels: number of tile levels in this image. + :sizeX: width of the image in pixels. + :sizeY: height of the image in pixels. + :tileWidth: width of a tile in pixels. + :tileHeight: height of a tile in pixels. + :magnification: if known, the magnificaiton of the image. + :mm_x: if known, the width of a pixel in millimeters. + :mm_y: if known, the height of a pixel in millimeters. + :dtype: if known, the type of values in this image. + + In addition to the keys that listed above, tile sources that expose + multiple frames will also contain + + :frames: a list of frames. Each frame entry is a dictionary with + + :Frame: a 0-values frame index (the location in the list) + :Channel: optional. The name of the channel, if known + :IndexC: optional if unique. A 0-based index into the channel + list + :IndexT: optional if unique. A 0-based index for time values + :IndexZ: optional if unique. A 0-based index for z values + :IndexXY: optional if unique. A 0-based index for view (xy) + values + :Index<axis>: optional if unique. A 0-based index for an + arbitrary axis. + :Index: a 0-based index of non-channel unique sets. If the + frames vary only by channel and are adjacent, they will + have the same index. + + :IndexRange: a dictionary of the number of unique index values from + frames if greater than 1 (e.g., if an entry like IndexXY is not + present, then all frames either do not have that value or have + a value of 0). + :IndexStride: a dictionary of the spacing between frames where + unique axes values change. + :channels: optional. If known, a list of channel names + :channelmap: optional. If known, a dictionary of channel names + with their offset into the channel list. + + Note that this does not include band information, though some tile + sources may do so. + """ + mag = self.getNativeMagnification() + return JSONDict({ + 'levels': self.levels, + 'sizeX': self.sizeX, + 'sizeY': self.sizeY, + 'tileWidth': self.tileWidth, + 'tileHeight': self.tileHeight, + 'magnification': mag['magnification'], + 'mm_x': mag['mm_x'], + 'mm_y': mag['mm_y'], + 'dtype': str(self.dtype), + 'bandCount': self.bandCount, + })
+ + + @property + def metadata(self) -> JSONDict: + return self.getMetadata() + + def _addMetadataFrameInformation( + self, metadata: JSONDict, channels: Optional[List[str]] = None) -> None: + """ + Given a metadata response that has a `frames` list, where each frame + has some of `Index(XY|Z|C|T)`, populate the `Frame`, `Index` and + possibly the `Channel` of each frame in the list and the `IndexRange`, + `IndexStride`, and possibly the `channels` and `channelmap` entries of + the metadata. + + :param metadata: the metadata response that might contain `frames`. + Modified. + :param channels: an optional list of channel names. + """ + if 'frames' not in metadata: + return + maxref: Dict[str, int] = {} + refkeys = {'IndexC'} + index = 0 + for idx, frame in enumerate(metadata['frames']): + refkeys |= {key for key in frame + if key.startswith('Index') and len(key.split('Index', 1)[1])} + for key in refkeys: + if key in frame and frame[key] + 1 > maxref.get(key, 0): + maxref[key] = frame[key] + 1 + frame['Frame'] = idx + if idx and (any( + frame.get(key) != metadata['frames'][idx - 1].get(key) + for key in refkeys if key != 'IndexC') or not any( + metadata['frames'][idx].get(key) for key in refkeys)): + index += 1 + frame['Index'] = index + if any(val > 1 for val in maxref.values()): + metadata['IndexRange'] = {key: value for key, value in maxref.items() if value > 1} + metadata['IndexStride'] = { + key: [idx for idx, frame in enumerate(metadata['frames']) if frame[key] == 1][0] + for key in metadata['IndexRange'] + } + if channels and len(channels) >= maxref.get('IndexC', 1): + metadata['channels'] = channels[:maxref.get('IndexC', 1)] + metadata['channelmap'] = { + cname: c for c, cname in enumerate(channels[:maxref.get('IndexC', 1)])} + for frame in metadata['frames']: + frame['Channel'] = channels[frame.get('IndexC', 0)] + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + return None
+ + +
+[docs] + def getOneBandInformation(self, band: int) -> Dict[str, Any]: + """ + Get band information for a single band. + + :param band: a 1-based band. + :returns: a dictionary of band information. See getBandInformation. + """ + return self.getBandInformation()[band]
+ + +
+[docs] + def getBandInformation(self, statistics: bool = False, **kwargs) -> Dict[int, Any]: + """ + Get information about each band in the image. + + :param statistics: if True, compute statistics if they don't already + exist. + :returns: a dictionary of one dictionary per band. Each dictionary + contains known values such as interpretation, min, max, mean, + stdev. + """ + if not getattr(self, '_bandInfo', None): + bandInterp = { + 1: ['gray'], + 2: ['gray', 'alpha'], + 3: ['red', 'green', 'blue'], + 4: ['red', 'green', 'blue', 'alpha']} + if not statistics: + if not getattr(self, '_bandInfoNoStats', None): + tile = cast(LazyTileDict, self.getSingleTile())['tile'] + bands = tile.shape[2] if len(tile.shape) > 2 else 1 + interp = bandInterp.get(bands, bandInterp[3]) + bandInfo = { + idx + 1: {'interpretation': interp[idx] if idx < len(interp) + else 'unknown'} for idx in range(bands)} + self._bandInfoNoStats = bandInfo + return self._bandInfoNoStats + analysisSize = 2048 + histogram = self.histogram( + onlyMinMax=True, + output={'maxWidth': min(self.sizeX, analysisSize), + 'maxHeight': min(self.sizeY, analysisSize)}, + resample=False, + **kwargs) + bands = histogram['min'].shape[0] + interp = bandInterp.get(bands, bandInterp[3]) + bandInfo = { + idx + 1: {'interpretation': interp[idx] if idx < len(interp) + else 'unknown'} for idx in range(bands)} + for key in {'min', 'max', 'mean', 'stdev'}: + if key in histogram: + for idx in range(bands): + bandInfo[idx + 1][key] = histogram[key][idx] + self._bandInfo = bandInfo + return self._bandInfo
+ + + def _getFrame(self, frame: Optional[int] = None, **kwargs) -> int: + """ + Get the current frame number. If a style is used that completely + specified the frame, use that value instead. + + :param frame: an integer or string with the frame number. + :returns: an integer frame number. + """ + frame = int(frame or 0) + if (hasattr(self, '_style') and 'bands' in self.style and + len(self.style['bands']) and + all(entry.get('frame') is not None for entry in self.style['bands'])): + frame = int(self.style['bands'][0]['frame']) + return frame + + def _xyzInRange( + self, x: int, y: int, z: int, frame: Optional[int] = None, + numFrames: Optional[int] = None) -> None: + """ + Check if a tile at x, y, z is in range based on self.levels, + self.tileWidth, self.tileHeight, self.sizeX, and self.sizeY, Raise an + ``TileSourceXYZRangeError`` exception if not. + """ + if z < 0 or z >= self.levels: + msg = 'z layer does not exist' + raise exceptions.TileSourceXYZRangeError(msg) + scale = 2 ** (self.levels - 1 - z) + offsetx = x * self.tileWidth * scale + if not (0 <= offsetx < self.sizeX): + msg = 'x is outside layer' + raise exceptions.TileSourceXYZRangeError(msg) + offsety = y * self.tileHeight * scale + if not (0 <= offsety < self.sizeY): + msg = 'y is outside layer' + raise exceptions.TileSourceXYZRangeError(msg) + if frame is not None and numFrames is not None: + if frame < 0 or frame >= numFrames: + msg = 'Frame does not exist' + raise exceptions.TileSourceXYZRangeError(msg) + + def _xyzToCorners(self, x: int, y: int, z: int) -> Tuple[int, int, int, int, int]: + """ + Convert a tile in x, y, z to corners and scale factor. The corners + are in full resolution image coordinates. The scale is always a power + of two >= 1. + + To convert the output to the resolution at the specified z level, + integer divide the corners by the scale (e.g., x0z = x0 // scale). + + :param x, y, z: the tile position. + :returns: x0, y0, x1, y1, scale. + """ + step = int(2 ** (self.levels - 1 - z)) + x0 = x * step * self.tileWidth + x1 = min((x + 1) * step * self.tileWidth, self.sizeX) + y0 = y * step * self.tileHeight + y1 = min((y + 1) * step * self.tileHeight, self.sizeY) + return x0, y0, x1, y1, step + + def _nonemptyLevelsList(self, frame: Optional[int] = 0) -> List[bool]: + """ + Return a list of one value per level where the value is None if the + level does not exist in the file and any other value if it does. + + :param frame: the frame number. + :returns: a list of levels length. + """ + return [True] * self.levels + + def _getTileFromEmptyLevel(self, x: int, y: int, z: int, **kwargs) -> PIL.Image.Image: + """ + Given the x, y, z tile location in an unpopulated level, get tiles from + higher resolution levels to make the lower-res tile. + + :param x: location of tile within original level. + :param y: location of tile within original level. + :param z: original level. + :returns: tile in PIL format. + """ + lastlog = time.time() + basez = z + scale = 1 + dirlist = self._nonemptyLevelsList(kwargs.get('frame')) + while dirlist[z] is None: + scale *= 2 + z += 1 + while z - basez > self._maxSkippedLevels: + z -= self._maxSkippedLevels + scale = int(scale / 2 ** self._maxSkippedLevels) + tile = PIL.Image.new('RGBA', ( + min(self.sizeX, self.tileWidth * scale), min(self.sizeY, self.tileHeight * scale))) + maxX = 2.0 ** (z + 1 - self.levels) * self.sizeX / self.tileWidth + maxY = 2.0 ** (z + 1 - self.levels) * self.sizeY / self.tileHeight + for newY in range(scale): + for newX in range(scale): + if ((newX or newY) and ((x * scale + newX) >= maxX or + (y * scale + newY) >= maxY)): + continue + if time.time() - lastlog > 10: + self.logger.info( + 'Compositing tile from higher resolution tiles x=%d y=%d z=%d', + x * scale + newX, y * scale + newY, z) + lastlog = time.time() + subtile = self.getTile( + x * scale + newX, y * scale + newY, z, + pilImageAllowed=True, numpyAllowed=False, + sparseFallback=True, edge=False, frame=kwargs.get('frame')) + subtile = _imageToPIL(subtile) + tile.paste(subtile, (newX * self.tileWidth, + newY * self.tileHeight)) + return tile.resize((self.tileWidth, self.tileHeight), + getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS) + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, + sparseFallback=False, frame=None): + """ + Get a tile from a tile source, returning it as an binary image, a PIL + image, or a numpy array. + + :param x: the 0-based x position of the tile on the specified z level. + 0 is left. + :param y: the 0-based y position of the tile on the specified z level. + 0 is top. + :param z: the z level of the tile. May range from [0, self.levels], + where 0 is the lowest resolution, single tile for the whole source. + :param pilImageAllowed: True if a PIL image may be returned. + :param numpyAllowed: True if a numpy image may be returned. 'always' + to return a numpy array. + :param sparseFallback: if False and a tile doesn't exist, raise an + error. If True, check if a lower resolution tile exists, and, if + so, interpolate the needed data for this tile. + :param frame: the frame number within the tile source. None is the + same as 0 for multi-frame sources. + :returns: either a numpy array, a PIL image, or a memory object with an + image file. + """ + raise NotImplementedError
+ + +
+[docs] + def getTileMimeType(self) -> str: + """ + Return the default mimetype for image tiles. + + :returns: the mime type of the tile. + """ + return TileOutputMimeTypes.get(self.encoding, 'image/jpeg')
+ + +
+[docs] + @methodcache() + def getThumbnail( + self, width: Optional[Union[str, int]] = None, + height: Optional[Union[str, int]] = None, **kwargs) -> Tuple[ + Union[np.ndarray, PIL.Image.Image, ImageBytes, bytes, pathlib.Path], str]: + """ + Get a basic thumbnail from the current tile source. Aspect ratio is + preserved. If neither width nor height is given, a default value is + used. If both are given, the thumbnail will be no larger than either + size. A thumbnail has the same options as a region except that it + always includes the entire image and has a default size of 256 x 256. + + :param width: maximum width in pixels. + :param height: maximum height in pixels. + :param kwargs: optional arguments. Some options are encoding, + jpegQuality, jpegSubsampling, and tiffCompression. + :returns: thumbData, thumbMime: the image data and the mime type. + """ + if ((width is not None and (not isinstance(width, int) or width < 2)) or + (height is not None and (not isinstance(height, int) or height < 2))): + msg = 'Invalid width or height. Minimum value is 2.' + raise ValueError(msg) + if width is None and height is None: + width = height = 256 + params = dict(kwargs) + params['output'] = {'maxWidth': width, 'maxHeight': height} + params.pop('region', None) + return self.getRegion(**params)
+ + +
+[docs] + def getPreferredLevel(self, level: int) -> int: + """ + Given a desired level (0 is minimum resolution, self.levels - 1 is max + resolution), return the level that contains actual data that is no + lower resolution. + + :param level: desired level + :returns level: a level with actual data that is no lower resolution. + """ + if self.levels is None: + return level + level = max(0, min(level, self.levels - 1)) + baselevel = level + levelList = self._nonemptyLevelsList() + while levelList[level] is None and level < self.levels - 1: + level += 1 + while level - baselevel >= self._maxSkippedLevels: + level -= self._maxSkippedLevels + return level
+ + +
+[docs] + def convertRegionScale( + self, sourceRegion: Dict[str, Any], + sourceScale: Optional[Dict[str, float]] = None, + targetScale: Optional[Dict[str, float]] = None, + targetUnits: Optional[str] = None, + cropToImage: bool = True) -> Dict[str, Any]: + """ + Convert a region from one scale to another. + + :param sourceRegion: a dictionary of optional values which specify the + part of an image to process. + + :left: the left edge (inclusive) of the region to process. + :top: the top edge (inclusive) of the region to process. + :right: the right edge (exclusive) of the region to process. + :bottom: the bottom edge (exclusive) of the region to process. + :width: the width of the region to process. + :height: the height of the region to process. + :units: either 'base_pixels' (default), 'pixels', 'mm', or + 'fraction'. base_pixels are in maximum resolution pixels. + pixels is in the specified magnification pixels. mm is in the + specified magnification scale. fraction is a scale of 0 to 1. + pixels and mm are only available if the magnification and mm + per pixel are defined for the image. + + :param sourceScale: a dictionary of optional values which specify the + scale of the source region. Required if the sourceRegion is + in "mag_pixels" units. + + :magnification: the magnification ratio. + :mm_x: the horizontal size of a pixel in millimeters. + :mm_y: the vertical size of a pixel in millimeters. + + :param targetScale: a dictionary of optional values which specify the + scale of the target region. Required in targetUnits is in + "mag_pixels" units. + + :magnification: the magnification ratio. + :mm_x: the horizontal size of a pixel in millimeters. + :mm_y: the vertical size of a pixel in millimeters. + + :param targetUnits: if not None, convert the region to these units. + Otherwise, the units are will either be the sourceRegion units if + those are not "mag_pixels" or base_pixels. If "mag_pixels", the + targetScale must be specified. + :param cropToImage: if True, don't return region coordinates outside of + the image. + """ + units = sourceRegion.get('units') + if units not in TileInputUnits: + raise ValueError('Invalid units %r' % units) + units = TileInputUnits[units] + if targetUnits is not None: + if targetUnits not in TileInputUnits: + raise ValueError('Invalid units %r' % targetUnits) + targetUnits = TileInputUnits[targetUnits] + if (units != 'mag_pixels' and ( + targetUnits is None or targetUnits == units)): + return sourceRegion + magArgs: Dict[str, Any] = (sourceScale or {}).copy() + magArgs['rounding'] = None + magLevel = self.getLevelForMagnification(**magArgs) + mag = self.getMagnificationForLevel(magLevel) + metadata = self.getMetadata() + # Get region in base pixels + left, top, right, bottom = self._getRegionBounds( + metadata, desiredMagnification=mag, cropToImage=cropToImage, + **sourceRegion) + # If requested, convert region to targetUnits + desMagArgs: Dict[str, Any] = (targetScale or {}).copy() + desMagArgs['rounding'] = None + desMagLevel = self.getLevelForMagnification(**desMagArgs) + desiredMagnification = self.getMagnificationForLevel(desMagLevel) + scaleX, scaleY = self._scaleFromUnits(metadata, targetUnits, desiredMagnification) + left = float(left) / scaleX + right = float(right) / scaleX + top = float(top) / scaleY + bottom = float(bottom) / scaleY + targetRegion = { + 'left': left, + 'top': top, + 'right': right, + 'bottom': bottom, + 'width': right - left, + 'height': bottom - top, + 'units': TileInputUnits[targetUnits], + } + # Reduce region information to match what was supplied + for key in ('left', 'top', 'right', 'bottom', 'width', 'height'): + if key not in sourceRegion: + del targetRegion[key] + return targetRegion
+ + +
+[docs] + def getRegion(self, format: Union[str, Tuple[str]] = (TILE_FORMAT_IMAGE, ), **kwargs) -> Tuple[ + Union[np.ndarray, PIL.Image.Image, ImageBytes, bytes, pathlib.Path], str]: + """ + Get a rectangular region from the current tile source. Aspect ratio is + preserved. If neither width nor height is given, the original size of + the highest resolution level is used. If both are given, the returned + image will be no larger than either size. + + :param format: the desired format or a tuple of allowed formats. + Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, + TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding may be + specified. + :param kwargs: optional arguments. Some options are region, output, + encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See + tileIterator. + :returns: regionData, formatOrRegionMime: the image data and either the + mime type, if the format is TILE_FORMAT_IMAGE, or the format. + """ + if not isinstance(format, (tuple, set, list)): + format = (format, ) + if 'tile_position' in kwargs: + kwargs = kwargs.copy() + kwargs.pop('tile_position', None) + tiled = TILE_FORMAT_IMAGE in format and kwargs.get('encoding') == 'TILED' + if not tiled and 'tile_offset' not in kwargs and 'tile_size' not in kwargs: + kwargs = kwargs.copy() + kwargs['tile_size'] = { + 'width': max(self.tileWidth, 4096), + 'height': max(self.tileHeight, 4096)} + kwargs['tile_offset'] = {'auto': True} + resample = True + if 'resample' in kwargs: + kwargs = kwargs.copy() + resample = kwargs.pop('resample', None) + tileIter = TileIterator(self, format=TILE_FORMAT_NUMPY, resample=None, **kwargs) + if tileIter.info is None: + pilimage = PIL.Image.new('RGB', (0, 0)) + return utilities._encodeImage(pilimage, format=format, **kwargs) + regionWidth = tileIter.info['region']['width'] + regionHeight = tileIter.info['region']['height'] + top = tileIter.info['region']['top'] + left = tileIter.info['region']['left'] + mode = None if TILE_FORMAT_NUMPY in format else tileIter.info['mode'] + outWidth = tileIter.info['output']['width'] + outHeight = tileIter.info['output']['height'] + image: Optional[Union[np.ndarray, PIL.Image.Image, ImageBytes, bytes]] = None + tiledimage = None + for tile in tileIter: + # Add each tile to the image + subimage, _ = _imageToNumpy(tile['tile']) + x0, y0 = tile['x'] - left, tile['y'] - top + if tiled: + tiledimage = utilities._addRegionTileToTiled( + tiledimage, subimage, x0, y0, regionWidth, regionHeight, tile, **kwargs) + else: + image = utilities._addSubimageToImage( + cast(Optional[np.ndarray], image), subimage, x0, y0, regionWidth, regionHeight) + # Somehow discarding the tile here speeds things up. + del tile + del subimage + # Scale if we need to + outWidth = int(math.floor(outWidth)) + outHeight = int(math.floor(outHeight)) + if tiled: + return self._encodeTiledImage( + cast(Dict[str, Any], tiledimage), outWidth, outHeight, tileIter.info, **kwargs) + if outWidth != regionWidth or outHeight != regionHeight: + dtype = cast(np.ndarray, image).dtype + if dtype == np.uint8 or resample is not None: + image = _imageToPIL(cast(np.ndarray, image), mode).resize( + (outWidth, outHeight), + getattr(PIL.Image, 'Resampling', PIL.Image).NEAREST + if resample is None else + getattr(PIL.Image, 'Resampling', PIL.Image).BICUBIC + if outWidth > regionWidth else + getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS) + if dtype == np.uint16 and TILE_FORMAT_NUMPY in format: + image = _imageToNumpy(image)[0].astype(dtype) * 257 + else: + cols = [int(idx * regionWidth / outWidth) for idx in range(outWidth)] + rows = [int(idx * regionHeight / outHeight) for idx in range(outHeight)] + image = np.take(np.take(image, rows, axis=0), cols, axis=1) + maxWidth = kwargs.get('output', {}).get('maxWidth') + maxHeight = kwargs.get('output', {}).get('maxHeight') + if kwargs.get('fill') and maxWidth and maxHeight: + image = utilities._letterboxImage( + _imageToPIL(cast(np.ndarray, image), mode), maxWidth, maxHeight, kwargs['fill']) + return utilities._encodeImage(cast(np.ndarray, image), format=format, **kwargs)
+ + + def _encodeTiledImage( + self, image: Dict[str, Any], outWidth: int, outHeight: int, + iterInfo: Dict[str, Any], **kwargs) -> Tuple[pathlib.Path, str]: + """ + Given an image record of a set of vips image strips, generate a tiled + tiff file at the specified output size. + + :param image: a record with partial vips images and the current output + size. + :param outWidth: the output size after scaling and before any + letterboxing. + :param outHeight: the output size after scaling and before any + letterboxing. + :param iterInfo: information about the region based on the tile + iterator. + + Additional parameters are available. + + :param fill: a color to use in letterboxing. + :param maxWidth: the output size if letterboxing is applied. + :param maxHeight: the output size if letterboxing is applied. + :param compression: the internal compression format. This can handle + a variety of options similar to the converter utility. + :returns: a pathlib.Path of the output file and the output mime type. + """ + import pyvips + + vimg = cast(pyvips.Image, image['strips'][0]) + for y in sorted(image['strips'].keys())[1:]: + if image['strips'][y].bands + 1 == vimg.bands: + image['strips'][y] = utilities._vipsAddAlphaBand(image['strips'][y], vimg) + elif vimg.bands + 1 == image['strips'][y].bands: + vimg = utilities._vipsAddAlphaBand(vimg, image['strips'][y]) + vimg = vimg.insert(image['strips'][y], 0, y, expand=True) + + if outWidth != image['width'] or outHeight != image['height']: + scale = outWidth / image['width'] + vimg = vimg.resize(outWidth / image['width'], vscale=outHeight / image['height']) + image['width'] = outWidth + image['height'] = outHeight + image['mm_x'] = image['mm_x'] / scale if image['mm_x'] else image['mm_x'] + image['mm_y'] = image['mm_y'] / scale if image['mm_y'] else image['mm_y'] + image['magnification'] = ( + image['magnification'] * scale + if image['magnification'] else image['magnification']) + return self._encodeTiledImageFromVips(vimg, iterInfo, image, **kwargs) + + def _encodeTiledImageFromVips( + self, vimg: Any, iterInfo: Dict[str, Any], image: Dict[str, Any], + **kwargs) -> Tuple[pathlib.Path, str]: + """ + Save a vips image as a tiled tiff. + + :param vimg: a vips image. + :param iterInfo: information about the region based on the tile + iterator. + :param image: a record with partial vips images and the current output + size. + + Additional parameters are available. + + :param compression: the internal compression format. This can handle + a variety of options similar to the converter utility. + :returns: a pathlib.Path of the output file and the output mime type. + """ + import pyvips + + convertParams = utilities._vipsParameters(defaultCompression='lzw', **kwargs) + vimg = utilities._vipsCast( + cast(pyvips.Image, vimg), convertParams['compression'] in {'webp', 'jpeg'}) + maxWidth = kwargs.get('output', {}).get('maxWidth') + maxHeight = kwargs.get('output', {}).get('maxHeight') + if (kwargs.get('fill') and str(kwargs.get('fill')).lower() != 'none' and + maxWidth and maxHeight and + (maxWidth > image['width'] or maxHeight > image['height'])): + corner: bool = False + fill: str = str(kwargs.get('fill')) + if fill.lower().startswith('corner:'): + corner, fill = True, fill.split(':', 1)[1] + color = PIL.ImageColor.getcolor( + fill, ['L', 'LA', 'RGB', 'RGBA'][vimg.bands - 1]) + if isinstance(color, int): + color = [color] + lbimage = pyvips.Image.black(maxWidth, maxHeight, bands=vimg.bands) + lbimage = lbimage.cast(vimg.format) + lbimage = lbimage.draw_rect( + [c * (257 if vimg.format == pyvips.BandFormat.USHORT else 1) for c in color], + 0, 0, maxWidth, maxHeight, fill=True) + vimg = lbimage.insert( + vimg, + (maxWidth - image['width']) // 2 if not corner else 0, + (maxHeight - image['height']) // 2 if not corner else 0) + if image['mm_x'] and image['mm_y']: + vimg = vimg.copy(xres=1 / image['mm_x'], yres=1 / image['mm_y']) + fd, outputPath = tempfile.mkstemp('.tiff', 'tiledRegion_') + os.close(fd) + try: + vimg.write_to_file(outputPath, **convertParams) + return pathlib.Path(outputPath), TileOutputMimeTypes['TILED'] + except Exception as exc: + try: + pathlib.Path(outputPath).unlink() + except Exception: + pass + raise exc + +
+[docs] + def tileFrames( + self, format: Union[str, Tuple[str]] = (TILE_FORMAT_IMAGE, ), + frameList: Optional[List[int]] = None, + framesAcross: Optional[int] = None, + max_workers: Optional[int] = -4, **kwargs) -> Tuple[ + Union[np.ndarray, PIL.Image.Image, ImageBytes, bytes, pathlib.Path], str]: + """ + Given the parameters for getRegion, plus a list of frames and the + number of frames across, make a larger image composed of a region from + each listed frame composited together. + + :param format: the desired format or a tuple of allowed formats. + Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, + TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding may be + specified. + :param frameList: None for all frames, or a list of 0-based integers. + :param framesAcross: the number of frames across the final image. If + unspecified, this is the ceiling of sqrt(number of frames in frame + list). + :param kwargs: optional arguments. Some options are region, output, + encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See + tileIterator. + :param max_workers: maximum workers for parallelism. If negative, use + the minimum of the absolute value of this number or + multiprocessing.cpu_count(). + :returns: regionData, formatOrRegionMime: the image data and either the + mime type, if the format is TILE_FORMAT_IMAGE, or the format. + """ + import concurrent.futures + + lastlog = time.time() + kwargs = kwargs.copy() + kwargs.pop('tile_position', None) + kwargs.pop('frame', None) + numFrames = len(self.getMetadata().get('frames', [0])) + if frameList: + frameList = [f for f in frameList if f >= 0 and f < numFrames] + if not frameList: + frameList = list(range(numFrames)) + if len(frameList) == 1: + return self.getRegion(format=format, frame=frameList[0], **kwargs) + if not framesAcross: + framesAcross = int(math.ceil(len(frameList) ** 0.5)) + framesAcross = min(len(frameList), framesAcross) + framesHigh = int(math.ceil(len(frameList) / framesAcross)) + if not isinstance(format, (tuple, set, list)): + format = (format, ) + tiled = TILE_FORMAT_IMAGE in format and kwargs.get('encoding') == 'TILED' + tileIter = TileIterator(self, format=TILE_FORMAT_NUMPY, resample=None, + frame=frameList[0], **kwargs) + if tileIter.info is None: + pilimage = PIL.Image.new('RGB', (0, 0)) + return utilities._encodeImage(pilimage, format=format, **kwargs) + frameWidth = tileIter.info['output']['width'] + frameHeight = tileIter.info['output']['height'] + maxWidth = kwargs.get('output', {}).get('maxWidth') + maxHeight = kwargs.get('output', {}).get('maxHeight') + if kwargs.get('fill') and maxWidth and maxHeight: + frameWidth, frameHeight = maxWidth, maxHeight + outWidth = frameWidth * framesAcross + outHeight = frameHeight * framesHigh + tile = next(tileIter) + image = None + tiledimage = None + if max_workers is not None and max_workers < 0: + max_workers = min(-max_workers, config.cpu_count(False)) + with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool: + futures = [] + for idx, frame in enumerate(frameList): + futures.append((idx, frame, pool.submit( + self.getRegion, format=TILE_FORMAT_NUMPY, frame=frame, **kwargs))) + for idx, frame, future in futures: + subimage, _ = future.result() + offsetX = (idx % framesAcross) * frameWidth + offsetY = (idx // framesAcross) * frameHeight + if time.time() - lastlog > 10: + self.logger.info( + 'Tiling frame %d (%d/%d), offset %dx%d', + frame, idx, len(frameList), offsetX, offsetY) + lastlog = time.time() + else: + self.logger.debug( + 'Tiling frame %d (%d/%d), offset %dx%d', + frame, idx, len(frameList), offsetX, offsetY) + if tiled: + tiledimage = utilities._addRegionTileToTiled( + tiledimage, cast(np.ndarray, subimage), offsetX, + offsetY, outWidth, outHeight, tile, **kwargs) + else: + image = utilities._addSubimageToImage( + image, cast(np.ndarray, subimage), offsetX, offsetY, + outWidth, outHeight) + if tiled: + return self._encodeTiledImage( + cast(Dict[str, Any], tiledimage), outWidth, outHeight, tileIter.info, **kwargs) + return utilities._encodeImage(cast(np.ndarray, image), format=format, **kwargs)
+ + +
+[docs] + def getRegionAtAnotherScale( + self, sourceRegion: Dict[str, Any], + sourceScale: Optional[Dict[str, float]] = None, + targetScale: Optional[Dict[str, float]] = None, + targetUnits: Optional[str] = None, **kwargs) -> Tuple[ + Union[np.ndarray, PIL.Image.Image, ImageBytes, bytes, pathlib.Path], str]: + """ + This takes the same parameters and returns the same results as + getRegion, except instead of region and scale, it takes sourceRegion, + sourceScale, targetScale, and targetUnits. These parameters are the + same as convertRegionScale. See those two functions for parameter + definitions. + """ + for key in ('region', 'scale'): + if key in kwargs: + raise TypeError('getRegionAtAnotherScale() got an unexpected ' + 'keyword argument of "%s"' % key) + region = self.convertRegionScale(sourceRegion, sourceScale, + targetScale, targetUnits) + return self.getRegion(region=region, scale=targetScale, **kwargs)
+ + +
+[docs] + def getPointAtAnotherScale( + self, point: Tuple[float, float], + sourceScale: Optional[Dict[str, float]] = None, + sourceUnits: Optional[str] = None, + targetScale: Optional[Dict[str, float]] = None, + targetUnits: Optional[str] = None, **kwargs) -> Tuple[float, float]: + """ + Given a point as a (x, y) tuple, convert it from one scale to another. + The sourceScale, sourceUnits, targetScale, and targetUnits parameters + are the same as convertRegionScale, where sourceUnits are the units + used with sourceScale. + """ + sourceRegion = { + 'units': 'base_pixels' if sourceUnits is None else sourceUnits, + 'left': point[0], + 'top': point[1], + 'right': point[0], + 'bottom': point[1], + } + region = self.convertRegionScale( + sourceRegion, sourceScale, targetScale, targetUnits, + cropToImage=False) + return (region['left'], region['top'])
+ + +
+[docs] + def getNativeMagnification(self) -> Dict[str, Optional[float]]: + """ + Get the magnification for the highest-resolution level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + return { + 'magnification': None, + 'mm_x': None, + 'mm_y': None, + }
+ + +
+[docs] + def getMagnificationForLevel(self, level: Optional[float] = None) -> Dict[str, Optional[float]]: + """ + Get the magnification at a particular level. + + :param level: None to use the maximum level, otherwise the level to get + the magnification factor of. + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + mag = self.getNativeMagnification() + + if level is not None and self.levels and level != self.levels - 1: + mag['scale'] = 2.0 ** (self.levels - 1 - level) + if mag['magnification']: + mag['magnification'] /= cast(float, mag['scale']) + if mag['mm_x'] and mag['mm_y']: + mag['mm_x'] *= cast(float, mag['scale']) + mag['mm_y'] *= cast(float, mag['scale']) + if self.levels: + mag['level'] = level if level is not None else self.levels - 1 + if mag.get('level') == self.levels - 1: + mag['scale'] = 1.0 + return mag
+ + +
+[docs] + def getLevelForMagnification( + self, magnification: Optional[float] = None, exact: bool = False, + mm_x: Optional[float] = None, mm_y: Optional[float] = None, + rounding: Optional[Union[str, bool]] = 'round', **kwargs, + ) -> Optional[Union[int, float]]: + """ + Get the level for a specific magnification or pixel size. If the + magnification is unknown or no level is sufficient resolution, and an + exact match is not requested, the highest level will be returned. + + If none of magnification, mm_x, and mm_y are specified, the maximum + level is returned. If more than one of these values is given, an + average of those given will be used (exact will require all of them to + match). + + :param magnification: the magnification ratio. + :param exact: if True, only a level that matches exactly will be + returned. + :param mm_x: the horizontal size of a pixel in millimeters. + :param mm_y: the vertical size of a pixel in millimeters. + :param rounding: if False, a fractional level may be returned. If + 'ceil' or 'round', that function is used to convert the level to an + integer (the exact flag still applies). If None, the level is not + cropped to the actual image's level range. + :returns: the selected level or None for no match. + """ + mag = self.getMagnificationForLevel() + ratios = [] + if magnification and mag['magnification']: + ratios.append(float(magnification) / mag['magnification']) + if mm_x and mag['mm_x']: + ratios.append(mag['mm_x'] / mm_x) + if mm_y and mag['mm_y']: + ratios.append(mag['mm_y'] / mm_y) + ratios = [math.log(ratio) / math.log(2) for ratio in ratios] + # Perform some slight rounding to handle numerical precision issues + ratios = [round(ratio, 4) for ratio in ratios] + if not len(ratios): + return mag.get('level', 0) + if exact: + if any(int(ratio) != ratio or ratio != ratios[0] + for ratio in ratios): + return None + ratio = round(sum(ratios) / len(ratios), 4) + level = (mag['level'] or 0) + ratio + if rounding: + level = int(math.ceil(level) if rounding == 'ceil' else + round(level)) + if (exact and (level > (mag['level'] or 0) or level < 0) or + (rounding == 'ceil' and level > (mag['level'] or 0))): + return None + if rounding is not None: + level = max(0, min(mag['level'] or 0, level)) + return level
+ + +
+[docs] + def tileIterator( + self, format: Union[str, Tuple[str]] = (TILE_FORMAT_NUMPY, ), + resample: bool = True, **kwargs) -> Iterator[LazyTileDict]: + """ + Iterate on all tiles in the specified region at the specified scale. + Each tile is returned as part of a dictionary that includes + + :x, y: (left, top) coordinates in current magnification pixels + :width, height: size of current tile in current magnification pixels + :tile: cropped tile image + :format: format of the tile + :level: level of the current tile + :level_x, level_y: the tile reference number within the level. + Tiles are numbered (0, 0), (1, 0), (2, 0), etc. The 0th tile + yielded may not be (0, 0) if a region is specified. + :tile_position: a dictionary of the tile position within the + iterator, containing: + + :level_x, level_y: the tile reference number within the level. + :region_x, region_y: 0, 0 is the first tile in the full + iteration (when not restricting the iteration to a single + tile). + :position: a 0-based value for the tile within the full + iteration. + + :iterator_range: a dictionary of the output range of the iterator: + + :level_x_min, level_x_max: the tiles that are be included + during the full iteration: [layer_x_min, layer_x_max). + :level_y_min, level_y_max: the tiles that are be included + during the full iteration: [layer_y_min, layer_y_max). + :region_x_max, region_y_max: the number of tiles included during + the full iteration. This is layer_x_max - layer_x_min, + layer_y_max - layer_y_min. + :position: the total number of tiles included in the full + iteration. This is region_x_max * region_y_max. + + :magnification: magnification of the current tile + :mm_x, mm_y: size of the current tile pixel in millimeters. + :gx, gy: (left, top) coordinates in maximum-resolution pixels + :gwidth, gheight: size of of the current tile in maximum-resolution + pixels. + :tile_overlap: the amount of overlap with neighboring tiles (left, + top, right, and bottom). Overlap never extends outside of the + requested region. + + If a region that includes partial tiles is requested, those tiles are + cropped appropriately. Most images will have tiles that get cropped + along the right and bottom edges in any case. If an exact + magnification or scale is requested, no tiles will be returned. + + :param format: the desired format or a tuple of allowed formats. + Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, + TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding must be + specified. + :param resample: If True or one of PIL.Image.Resampling.NEAREST, + LANCZOS, BILINEAR, or BICUBIC to resample tiles that are not the + target output size. Tiles that are resampled will have additional + dictionary entries of: + + :scaled: the scaling factor that was applied (less than 1 is + downsampled). + :tile_x, tile_y: (left, top) coordinates before scaling + :tile_width, tile_height: size of the current tile before + scaling. + :tile_magnification: magnification of the current tile before + scaling. + :tile_mm_x, tile_mm_y: size of a pixel in a tile in millimeters + before scaling. + + Note that scipy.misc.imresize uses PIL internally. + :param region: a dictionary of optional values which specify the part + of the image to process: + + :left: the left edge (inclusive) of the region to process. + :top: the top edge (inclusive) of the region to process. + :right: the right edge (exclusive) of the region to process. + :bottom: the bottom edge (exclusive) of the region to process. + :width: the width of the region to process. + :height: the height of the region to process. + :units: either 'base_pixels' (default), 'pixels', 'mm', or + 'fraction'. base_pixels are in maximum resolution pixels. + pixels is in the specified magnification pixels. mm is in the + specified magnification scale. fraction is a scale of 0 to 1. + pixels and mm are only available if the magnification and mm + per pixel are defined for the image. + + :param output: a dictionary of optional values which specify the size + of the output. + + :maxWidth: maximum width in pixels. If either maxWidth or maxHeight + is specified, magnification, mm_x, and mm_y are ignored. + :maxHeight: maximum height in pixels. + + :param scale: a dictionary of optional values which specify the scale + of the region and / or output. This applies to region if + pixels or mm are used for inits. It applies to output if + neither output maxWidth nor maxHeight is specified. + + :magnification: the magnification ratio. Only used if maxWidth and + maxHeight are not specified or None. + :mm_x: the horizontal size of a pixel in millimeters. + :mm_y: the vertical size of a pixel in millimeters. + :exact: if True, only a level that matches exactly will be returned. + This is only applied if magnification, mm_x, or mm_y is used. + + :param tile_position: if present, either a number to only yield the + (tile_position)th tile [0 to (xmax - min) * (ymax - ymin)) that the + iterator would yield, or a dictionary of {region_x, region_y} to + yield that tile, where 0, 0 is the first tile yielded, and + xmax - xmin - 1, ymax - ymin - 1 is the last tile yielded, or a + dictionary of {level_x, level_y} to yield that specific tile if it + is in the region. + :param tile_size: if present, retile the output to the specified tile + size. If only width or only height is specified, the resultant + tiles will be square. This is a dictionary containing at least + one of: + + :width: the desired tile width. + :height: the desired tile height. + + :param tile_overlap: if present, retile the output adding a symmetric + overlap to the tiles. If either x or y is not specified, it + defaults to zero. The overlap does not change the tile size, + only the stride of the tiles. This is a dictionary containing: + + :x: the horizontal overlap in pixels. + :y: the vertical overlap in pixels. + :edges: if True, then the edge tiles will exclude the overlap + distance. If unset or False, the edge tiles are full size. + + The overlap is conceptually split between the two sides of + the tile. This is only relevant to where overlap is reported + or if edges is True + + As an example, suppose an image that is 8 pixels across + (01234567) and a tile size of 5 is requested with an overlap of + 4. If the edges option is False (the default), the following + tiles are returned: 01234, 12345, 23456, 34567. Each tile + reports its overlap, and the non-overlapped area of each tile + is 012, 3, 4, 567. If the edges option is True, the tiles + returned are: 012, 0123, 01234, 12345, 23456, 34567, 4567, 567, + with the non-overlapped area of each as 0, 1, 2, 3, 4, 5, 6, 7. + + :param tile_offset: if present, adjust tile positions so that the + corner of one tile is at the specified location. + + :left: the left offset in pixels. + :top: the top offset in pixels. + :auto: a boolean, if True, automatically set the offset to align + with the region's left and top. + + :param encoding: if format includes TILE_FORMAT_IMAGE, a valid PIL + encoding (typically 'PNG', 'JPEG', or 'TIFF') or 'TILED' (identical + to TIFF). Must also be in the TileOutputMimeTypes map. + :param jpegQuality: the quality to use when encoding a JPEG. + :param jpegSubsampling: the subsampling level to use when encoding a + JPEG. + :param tiffCompression: the compression format when encoding a TIFF. + This is usually 'raw', 'tiff_lzw', 'jpeg', or 'tiff_adobe_deflate'. + Some of these are aliased: 'none', 'lzw', 'deflate'. + :param frame: the frame number within the tile source. None is the + same as 0 for multi-frame sources. + :param kwargs: optional arguments. + :yields: an iterator that returns a dictionary as listed above. + """ + return TileIterator(self, format=format, resample=resample, **kwargs)
+ + +
+[docs] + def tileIteratorAtAnotherScale( + self, sourceRegion: Dict[str, Any], + sourceScale: Optional[Dict[str, float]] = None, + targetScale: Optional[Dict[str, float]] = None, + targetUnits: Optional[str] = None, **kwargs) -> Iterator[LazyTileDict]: + """ + This takes the same parameters and returns the same results as + tileIterator, except instead of region and scale, it takes + sourceRegion, sourceScale, targetScale, and targetUnits. These + parameters are the same as convertRegionScale. See those two functions + for parameter definitions. + """ + for key in ('region', 'scale'): + if key in kwargs: + raise TypeError('getRegionAtAnotherScale() got an unexpected ' + 'keyword argument of "%s"' % key) + region = self.convertRegionScale(sourceRegion, sourceScale, + targetScale, targetUnits) + return self.tileIterator(region=region, scale=targetScale, **kwargs)
+ + +
+[docs] + def getSingleTile(self, *args, **kwargs) -> Optional[LazyTileDict]: + """ + Return any single tile from an iterator. This takes exactly the same + parameters as tileIterator. Use tile_position to get a specific tile, + otherwise the first tile is returned. + + :return: a tile dictionary or None. + """ + return next(self.tileIterator(*args, **kwargs), None)
+ + +
+[docs] + def getSingleTileAtAnotherScale(self, *args, **kwargs) -> Optional[LazyTileDict]: + """ + Return any single tile from a rescaled iterator. This takes exactly + the same parameters as tileIteratorAtAnotherScale. Use tile_position + to get a specific tile, otherwise the first tile is returned. + + :return: a tile dictionary or None. + """ + return next(self.tileIteratorAtAnotherScale(*args, **kwargs), None)
+ + +
+[docs] + def getTileCount(self, *args, **kwargs) -> int: + """ + Return the number of tiles that the tileIterator will return. See + tileIterator for parameters. + + :return: the number of tiles that the tileIterator will yield. + """ + tile = next(self.tileIterator(*args, **kwargs), None) + if tile is not None: + return tile['iterator_range']['position'] + return 0
+ + +
+[docs] + def getAssociatedImagesList(self) -> List[str]: + """ + Return a list of associated images. + + :return: the list of image keys. + """ + return []
+ + +
+[docs] + def getAssociatedImage( + self, imageKey: str, *args, **kwargs) -> Optional[Tuple[ImageBytes, str]]: + """ + Return an associated image. + + :param imageKey: the key of the associated image to retrieve. + :param kwargs: optional arguments. Some options are width, height, + encoding, jpegQuality, jpegSubsampling, and tiffCompression. + :returns: imageData, imageMime: the image data and the mime type, or + None if the associated image doesn't exist. + """ + image = self._getAssociatedImage(imageKey) + if not image: + return None + imageWidth, imageHeight = image.size + width = kwargs.get('width') + height = kwargs.get('height') + if width or height: + width, height, calcScale = utilities._calculateWidthHeight( + width, height, imageWidth, imageHeight) + image = image.resize( + (width, height), + getattr(PIL.Image, 'Resampling', PIL.Image).BICUBIC + if width > imageWidth else + getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS) + return cast(Tuple[ImageBytes, str], utilities._encodeImage(image, **kwargs))
+ + +
+[docs] + def getPixel(self, includeTileRecord: bool = False, **kwargs) -> JSONDict: + """ + Get a single pixel from the current tile source. + + :param includeTileRecord: if True, include the tile used for computing + the pixel in the response. + :param kwargs: optional arguments. Some options are region, output, + encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See + tileIterator. + :returns: a dictionary with the value of the pixel for each channel on + a scale of [0-255], including alpha, if available. This may + contain additional information. + """ + regionArgs = kwargs.copy() + regionArgs['region'] = regionArgs.get('region', {}).copy() + regionArgs['region']['width'] = regionArgs['region']['height'] = 1 + regionArgs['region']['unitsWH'] = 'base_pixels' + pixel: Dict[str, Any] = {} + # This could be + # img, format = self.getRegion(format=TILE_FORMAT_PIL, **regionArgs) + # where img is the PIL image (rather than tile['tile'], but using + # TileIterator is slightly more efficient. + tile = next(TileIterator(self, format=TILE_FORMAT_NUMPY, **regionArgs), None) + if tile is None: + return JSONDict(pixel) + if includeTileRecord: + pixel['tile'] = tile + pixel['value'] = [v.item() for v in tile['tile'][0][0]] + img = _imageToPIL(tile['tile']) + if img.size[0] >= 1 and img.size[1] >= 1: + if len(img.mode) > 1: + pixel.update(dict(zip(img.mode.lower(), img.load()[0, 0]))) + else: + pixel.update(dict(zip([img.mode.lower()], [img.load()[0, 0]]))) + return JSONDict(pixel)
+ + + @property + def frames(self) -> int: + """A property with the number of frames.""" + if not hasattr(self, '_frameCount'): + self._frameCount = len(self.getMetadata().get('frames', [])) or 1 + return self._frameCount
+ + + +
+[docs] +class FileTileSource(TileSource): + + def __init__( + self, path: Union[str, pathlib.Path, Dict[Any, Any]], *args, **kwargs) -> None: + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(*args, **kwargs) + # Expand the user without converting datatype of path. + try: + path = (cast(pathlib.Path, path).expanduser() + if callable(getattr(path, 'expanduser', None)) else + os.path.expanduser(cast(str, path))) + except TypeError: + # Don't fail if the path is unusual -- maybe a source can handle it + pass + self.largeImagePath = path + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs) -> str: + return strhash( + args[0], kwargs.get('encoding', 'JPEG'), kwargs.get('jpegQuality', 95), + kwargs.get('jpegSubsampling', 0), kwargs.get('tiffCompression', 'raw'), + kwargs.get('edge', False), + '__STYLESTART__', kwargs.get('style', None), '__STYLEEND__')
+ + +
+[docs] + def getState(self) -> str: + if hasattr(self, '_classkey'): + return self._classkey + return '%s,%s,%s,%s,%s,%s,__STYLESTART__,%s,__STYLE_END__' % ( + self._getLargeImagePath(), + self.encoding, + self.jpegQuality, + self.jpegSubsampling, + self.tiffCompression, + self.edge, + self._jsonstyle)
+ + + def _getLargeImagePath(self) -> Union[str, pathlib.Path, Dict[Any, Any]]: + return self.largeImagePath + +
+[docs] + @classmethod + def canRead(cls, path: Union[str, pathlib.Path, Dict[Any, Any]], *args, **kwargs) -> bool: + """ + Check if we can read the input. This takes the same parameters as + __init__. + + :returns: True if this class can read the input. False if it + cannot. + """ + try: + cls(path, *args, **kwargs) + return True + except exceptions.TileSourceError: + return False
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource/geo.html b/_modules/large_image/tilesource/geo.html new file mode 100644 index 000000000..01d57911d --- /dev/null +++ b/_modules/large_image/tilesource/geo.html @@ -0,0 +1,603 @@ + + + + + + large_image.tilesource.geo — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource.geo

+import pathlib
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
+from urllib.parse import urlencode, urlparse
+
+import numpy as np
+import PIL.Image
+
+from ..cache_util import CacheProperties, methodcache
+from ..constants import SourcePriority, TileInputUnits
+from ..exceptions import TileSourceError
+from .base import FileTileSource
+from .utilities import ImageBytes, JSONDict, getPaletteColors
+
+# Inform the tile source cache about the potential size of this tile source
+CacheProperties['tilesource']['itemExpectedSize'] = max(
+    CacheProperties['tilesource']['itemExpectedSize'],
+    100 * 1024 ** 2)
+
+# Used to cache pixel size for projections
+ProjUnitsAcrossLevel0: Dict[str, float] = {}
+ProjUnitsAcrossLevel0_MaxSize = 100
+
+
+
+[docs] +def make_vsi(url: Union[str, pathlib.Path, Dict[Any, Any]], **options) -> str: + if str(url).startswith('s3://'): + s3_path = str(url).replace('s3://', '') + vsi = f'/vsis3/{s3_path}' + else: + gdal_options = { + 'url': str(url), + 'use_head': 'no', + 'list_dir': 'no', + } + gdal_options.update(options) + vsi = f'/vsicurl?{urlencode(gdal_options)}' + return vsi
+ + + +
+[docs] +class GeoBaseFileTileSource(FileTileSource): + """Abstract base class for geospatial tile sources.""" + + _geospatial_source = True
+ + + +
+[docs] +class GDALBaseFileTileSource(GeoBaseFileTileSource): + """Abstract base class for GDAL-based tile sources. + + This base class assumes the underlying library is powered by GDAL + (rasterio, mapnik, etc.) + """ + + _unstyledStyle = '{}' + + extensions = { + None: SourcePriority.MEDIUM, + 'geotiff': SourcePriority.PREFERRED, + # National Imagery Transmission Format + 'ntf': SourcePriority.PREFERRED, + 'nitf': SourcePriority.PREFERRED, + 'tif': SourcePriority.LOW, + 'tiff': SourcePriority.LOW, + 'vrt': SourcePriority.PREFERRED, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'image/geotiff': SourcePriority.PREFERRED, + 'image/tiff': SourcePriority.LOW, + 'image/x-tiff': SourcePriority.LOW, + } + + projection: Union[str, bytes] + projectionOrigin: Tuple[float, float] + sourceLevels: int + sourceSizeX: int + sourceSizeY: int + unitsAcrossLevel0: float + + def _getDriver(self) -> str: + """ + Get the GDAL driver used to read this dataset. + + :returns: The name of the driver. + """ + raise NotImplementedError + + def _convertProjectionUnits(self, *args, **kwargs) -> Tuple[float, float, float, float, str]: + raise NotImplementedError + +
+[docs] + def pixelToProjection(self, *args, **kwargs) -> Tuple[float, float]: + raise NotImplementedError
+ + +
+[docs] + def toNativePixelCoordinates(self, *args, **kwargs) -> Tuple[float, float]: + raise NotImplementedError
+ + +
+[docs] + def getBounds(self, *args, **kwargs) -> Dict[str, Any]: + raise NotImplementedError
+ + +
+[docs] + @staticmethod + def isGeospatial(path: Union[str, pathlib.Path]) -> bool: + """ + Check if a path is likely to be a geospatial file. + + :param path: The path to the file + :returns: True if geospatial. + """ + raise NotImplementedError
+ + + @property + def geospatial(self) -> bool: + """ + This is true if the source has geospatial information. + """ + return bool(self.projection) + + def _getLargeImagePath(self) -> str: + """Get GDAL-compatible image path. + + This will cast the output to a string and can also handle URLs + ('http', 'https', 'ftp', 's3') for use with GDAL + `Virtual Filesystems Interface <https://gdal.org/user/virtual_file_systems.html>`_. + """ + if urlparse(str(self.largeImagePath)).scheme in {'http', 'https', 'ftp', 's3'}: + return make_vsi(self.largeImagePath) + return str(self.largeImagePath) + + def _setStyle(self, style: Any) -> None: + """ + Check and set the specified style from a json string or a dictionary. + + :param style: The new style. + """ + super()._setStyle(style) + if hasattr(self, '_getTileLock'): + self._setDefaultStyle() + + def _styleBands(self) -> List[Dict[str, Any]]: + interpColorTable = { + 'red': ['#000000', '#ff0000'], + 'green': ['#000000', '#00ff00'], + 'blue': ['#000000', '#0000ff'], + 'gray': ['#000000', '#ffffff'], + 'alpha': ['#ffffff00', '#ffffffff'], + } + style = [] + if hasattr(self, '_style'): + styleBands = self.style['bands'] if 'bands' in self.style else [self.style] + for styleBand in styleBands: + + styleBand = styleBand.copy() + # Default to band 1 -- perhaps we should default to gray or + # green instead. + styleBand['band'] = self._bandNumber(styleBand.get('band', 1)) + style.append(styleBand) + if not len(style): + for interp in ('red', 'green', 'blue', 'gray', 'palette', 'alpha'): + band = self._bandNumber(interp, False) + # If we don't have the requested band, or we only have alpha, + # or this is gray or palette and we already added another band, + # skip this interpretation. + if (band is None or + (interp == 'alpha' and not len(style)) or + (interp in ('gray', 'palette') and len(style))): + continue + if interp == 'palette': + bandInfo = self.getOneBandInformation(band) + style.append({ + 'band': band, + 'palette': 'colortable', + 'min': 0, + 'max': len(bandInfo['colortable']) - 1}) + else: + style.append({ + 'band': band, + 'palette': interpColorTable[interp], + 'min': 'auto', + 'max': 'auto', + 'nodata': 'auto', + 'composite': 'multiply' if interp == 'alpha' else 'lighten', + }) + return style + + def _setDefaultStyle(self) -> None: + """If no style was specified, create a default style.""" + self._bandNames = {} + for idx, band in self.getBandInformation().items(): + if band.get('interpretation'): + self._bandNames[band['interpretation'].lower()] = idx + if isinstance(getattr(self, '_style', None), dict) and ( + not self._style or 'icc' in self._style and len(self._style) == 1): + return + if hasattr(self, '_style'): + styleBands = self.style['bands'] if 'bands' in self.style else [self.style] + if not len(styleBands) or (len(styleBands) == 1 and isinstance( + styleBands[0].get('band', 1), int) and styleBands[0].get('band', 1) <= 0): + del self._style + style = self._styleBands() + if len(style): + hasAlpha = False + for bstyle in style: + hasAlpha = hasAlpha or self.getOneBandInformation( + bstyle.get('band', 0)).get('interpretation') == 'alpha' + if 'palette' in bstyle: + if bstyle['palette'] == 'colortable': + bandInfo = self.getOneBandInformation(bstyle.get('band', 0)) + bstyle['palette'] = [( + '#%02X%02X%02X' if len(entry) == 3 else + '#%02X%02X%02X%02X') % entry for entry in bandInfo['colortable']] + else: + bstyle['palette'] = self.getHexColors(bstyle['palette']) + if bstyle.get('nodata') == 'auto': + bandInfo = self.getOneBandInformation(bstyle.get('band', 0)) + bstyle['nodata'] = bandInfo.get('nodata', None) + if not hasAlpha and self.projection: + style.append({ + 'band': ( + self._bandNumber('alpha', False) + if self._bandNumber('alpha', False) is not None else + (len(self.getBandInformation()) + 1)), + 'min': 0, + 'max': 'auto', + 'composite': 'multiply', + 'palette': ['#ffffff00', '#ffffffff'], + }) + self.logger.debug('Using style %r', style) + self._style = JSONDict({'bands': style}) + +
+[docs] + @staticmethod + def getHexColors(palette: Union[str, List[Union[str, float, Tuple[float, ...]]]]) -> List[str]: + """ + Returns list of hex colors for a given color palette + + :returns: List of colors + """ + pal = getPaletteColors(palette) + return ['#%02X%02X%02X%02X' % tuple(int(val) for val in clr) for clr in pal]
+ + +
+[docs] + def getPixelSizeInMeters(self) -> Optional[float]: + """ + Get the approximate base pixel size in meters. This is calculated as + the average scale of the four edges in the WGS84 ellipsoid. + + :returns: the pixel size in meters or None. + """ + bounds = self.getBounds('epsg:4326') + if not bounds: + return None + try: + import pyproj + + geod = pyproj.Geod(ellps='WGS84') + computer = cast(Callable[[Any, Any, Any, Any], Tuple[Any, Any, float]], geod.inv) + except Exception: + # Estimate based on great-circle distance + def computer(lon1, lat1, lon2, lat2) -> Tuple[Any, Any, float]: + from math import acos, cos, radians, sin + lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) + return None, None, 6.378e+6 * ( + acos(sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(lon1 - lon2)) + ) + _, _, s1 = computer(bounds['ul']['x'], bounds['ul']['y'], + bounds['ur']['x'], bounds['ur']['y']) + _, _, s2 = computer(bounds['ur']['x'], bounds['ur']['y'], + bounds['lr']['x'], bounds['lr']['y']) + _, _, s3 = computer(bounds['lr']['x'], bounds['lr']['y'], + bounds['ll']['x'], bounds['ll']['y']) + _, _, s4 = computer(bounds['ll']['x'], bounds['ll']['y'], + bounds['ul']['x'], bounds['ul']['y']) + return (s1 + s2 + s3 + s4) / (self.sourceSizeX * 2 + self.sourceSizeY * 2)
+ + +
+[docs] + def getNativeMagnification(self) -> Dict[str, Optional[float]]: + """ + Get the magnification at the base level. + + :return: width of a pixel in mm, height of a pixel in mm. + """ + scale = self.getPixelSizeInMeters() + return { + 'magnification': None, + 'mm_x': scale * 100 if scale else None, + 'mm_y': scale * 100 if scale else None, + }
+ + +
+[docs] + def getTileCorners(self, z: int, x: float, y: float) -> Tuple[float, float, float, float]: + """ + Returns bounds of a tile for a given x,y,z index. + + :param z: tile level + :param x: tile offset from left. + :param y: tile offset from right + :returns: (xmin, ymin, xmax, ymax) in the current projection or base + pixels. + """ + x, y = float(x), float(y) + if self.projection: + # Scale tile into the range [-0.5, 0.5], [-0.5, 0.5] + xmin = -0.5 + x / 2.0 ** z + xmax = -0.5 + (x + 1) / 2.0 ** z + ymin = 0.5 - (y + 1) / 2.0 ** z + ymax = 0.5 - y / 2.0 ** z + # Convert to projection coordinates + xmin = self.projectionOrigin[0] + xmin * self.unitsAcrossLevel0 + xmax = self.projectionOrigin[0] + xmax * self.unitsAcrossLevel0 + ymin = self.projectionOrigin[1] + ymin * self.unitsAcrossLevel0 + ymax = self.projectionOrigin[1] + ymax * self.unitsAcrossLevel0 + else: + xmin = 2 ** (self.sourceLevels - 1 - z) * x * self.tileWidth + ymin = 2 ** (self.sourceLevels - 1 - z) * y * self.tileHeight + xmax = xmin + 2 ** (self.sourceLevels - 1 - z) * self.tileWidth + ymax = ymin + 2 ** (self.sourceLevels - 1 - z) * self.tileHeight + ymin, ymax = self.sourceSizeY - ymax, self.sourceSizeY - ymin + return xmin, ymin, xmax, ymax
+ + + def _bandNumber(self, band: Optional[Union[str, int]], exc: bool = True) -> Optional[int]: + """Given a band number or interpretation name, return a validated band number. + + :param band: either -1, a positive integer, or the name of a band interpretation + that is present in the tile source. + :param exc: if True, raise an exception if no band matches. + + :returns: a validated band, either 1 or a positive integer, or None if no + matching band and exceptions are not enabled. + """ + # retrieve the bands information from the initial dataset or cache + bands = self.getBandInformation() + + # search for the band with multiple methods + if isinstance(band, str) and str(band).isdigit(): + band = int(band) + elif isinstance(band, str): + band = next((i for i in bands if band == bands[i]['interpretation']), None) + + # set to None if not included in the possible band values + isBandNumber = band == -1 or band in bands + band = band if isBandNumber else None + + # raise an error if the band is not inside the dataset only if + # requested from the function call + if exc is True and band is None: + msg = ('Band has to be a positive integer, -1, or a band ' + 'interpretation found in the source.') + raise TileSourceError(msg) + + return band + + def _getRegionBounds( + self, metadata: JSONDict, left: Optional[float] = None, + top: Optional[float] = None, right: Optional[float] = None, + bottom: Optional[float] = None, width: Optional[float] = None, + height: Optional[float] = None, units: Optional[str] = None, + desiredMagnification: Optional[Dict[str, Optional[float]]] = None, + cropToImage: bool = True, **kwargs) -> Tuple[float, float, float, float]: + """ + Given a set of arguments that can include left, right, top, bottom, + width, height, and units, generate actual pixel values for left, top, + right, and bottom. If units is `'projection'`, use the source's + projection. If units starts with `'proj4:'` or `'epsg:'` or a + custom units value, use that projection. Otherwise, just use the super + function. + + :param metadata: the metadata associated with this source. + :param left: the left edge (inclusive) of the region to process. + :param top: the top edge (inclusive) of the region to process. + :param right: the right edge (exclusive) of the region to process. + :param bottom: the bottom edge (exclusive) of the region to process. + :param width: the width of the region to process. Ignored if both + left and right are specified. + :param height: the height of the region to process. Ignores if both + top and bottom are specified. + :param units: either 'projection', a string starting with 'proj4:', + 'epsg:' or a enumerated value like 'wgs84', or one of the super's + values. + :param kwargs: optional parameters. See above. + :returns: left, top, right, bottom bounds in pixels. + """ + units = TileInputUnits.get(units.lower() if units else units, units) + # If a proj4 projection is specified, convert the left, right, top, and + # bottom to the current projection or to pixel space if no projection + # is used. + if (units and (units.lower().startswith('proj4:') or + units.lower().startswith('epsg:') or + units.lower().startswith('+proj='))): + left, top, right, bottom, units = self._convertProjectionUnits( + left, top, right, bottom, width, height, units, **kwargs) + + if units == 'projection' and self.projection: + bounds = self.getBounds(self.projection) + # Fill in missing values + if left is None: + left = bounds['xmin'] if right is None or width is None else right - width + if right is None: + right = bounds['xmax'] if width is None else left + width + if top is None: + top = bounds['ymax'] if bottom is None or width is None else bottom - width + if bottom is None: + bottom = bounds['ymin'] if width is None else cast(float, top) + cast(float, height) + if not kwargs.get('unitsWH') or kwargs.get('unitsWH') == units: + width = height = None + # Convert to [-0.5, 0.5], [-0.5, 0.5] coordinate range + left = (left - self.projectionOrigin[0]) / self.unitsAcrossLevel0 + right = (right - self.projectionOrigin[0]) / self.unitsAcrossLevel0 + top = (top - self.projectionOrigin[1]) / self.unitsAcrossLevel0 + bottom = (bottom - self.projectionOrigin[1]) / self.unitsAcrossLevel0 + # Convert to world=wide 'base pixels' and crop to the world + xScale = 2 ** (self.levels - 1) * self.tileWidth + yScale = 2 ** (self.levels - 1) * self.tileHeight + left = max(0, min(xScale, (0.5 + left) * xScale)) + right = max(0, min(xScale, (0.5 + right) * xScale)) + top = max(0, min(yScale, (0.5 - top) * yScale)) + bottom = max(0, min(yScale, (0.5 - bottom) * yScale)) + # Ensure correct ordering + left, right = min(cast(float, left), cast(float, right)), max(left, cast(float, right)) + top, bottom = min(cast(float, top), cast(float, bottom)), max(top, cast(float, bottom)) + units = 'base_pixels' + return super()._getRegionBounds( + metadata, left, top, right, bottom, width, height, units, + desiredMagnification, cropToImage, **kwargs) + +
+[docs] + @methodcache() + def getThumbnail( + self, width: Optional[Union[str, int]] = None, + height: Optional[Union[str, int]] = None, **kwargs) -> Tuple[ + Union[np.ndarray, PIL.Image.Image, ImageBytes, bytes, pathlib.Path], str]: + """ + Get a basic thumbnail from the current tile source. Aspect ratio is + preserved. If neither width nor height is given, a default value is + used. If both are given, the thumbnail will be no larger than either + size. A thumbnail has the same options as a region except that it + always includes the entire image if there is no projection and has a + default size of 256 x 256. + + :param width: maximum width in pixels. + :param height: maximum height in pixels. + :param kwargs: optional arguments. Some options are encoding, + jpegQuality, jpegSubsampling, and tiffCompression. + :returns: thumbData, thumbMime: the image data and the mime type. + """ + if self.projection: + if ((width is not None and float(width) < 2) or + (height is not None and float(height) < 2)): + msg = 'Invalid width or height. Minimum value is 2.' + raise ValueError(msg) + if width is None and height is None: + width = height = 256 + params = dict(kwargs) + params['output'] = {'maxWidth': width, 'maxHeight': height} + params['region'] = {'units': 'projection'} + return self.getRegion(**params) + return super().getThumbnail(width, height, **kwargs)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource/jupyter.html b/_modules/large_image/tilesource/jupyter.html new file mode 100644 index 000000000..b37bcfad5 --- /dev/null +++ b/_modules/large_image/tilesource/jupyter.html @@ -0,0 +1,621 @@ + + + + + + large_image.tilesource.jupyter — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource.jupyter

+"""A vanilla REST interface to a ``TileSource``.
+
+This is intended for use in JupyterLab and not intended to be used as a full
+fledged REST API. Only two endpoints are exposed with minimal options:
+
+* `/metadata`
+* `/tile?z={z}&x={x}&y={y}&encoding=png`
+
+We use Tornado because it is Jupyter's web server and will not require Jupyter
+users to install any additional dependencies. Also, Tornado doesn't require us
+to manage a separate thread for the web server.
+
+Please note that this webserver will not work with Classic Notebook and will
+likely lead to crashes. This is only for use in JupyterLab.
+
+"""
+import importlib.util
+import json
+import os
+import re
+import weakref
+from typing import Any, Dict, List, Optional, Tuple, Union, cast
+
+from large_image.exceptions import TileSourceXYZRangeError
+from large_image.tilesource.utilities import JSONDict
+
+ipyleafletPresent = importlib.util.find_spec('ipyleaflet') is not None
+
+
+
+[docs] +class IPyLeafletMixin: + """Mixin class to support interactive visualization in JupyterLab. + + This class implements ``_ipython_display_`` with ``ipyleaflet`` + to display an interactive image visualizer for the tile source + in JupyterLab. + + Install `ipyleaflet <https://github.com/jupyter-widgets/ipyleaflet>`_ + to interactively visualize tile sources in JupyterLab. + + For remote JupyterHub environments, you may need to configure + the class variables ``JUPYTER_HOST`` or ``JUPYTER_PROXY``. + + If ``JUPYTER_PROXY`` is set, it overrides ``JUPYTER_HOST``. + + Use ``JUPYTER_HOST`` to set the host name of the machine such + that the tile URL can be accessed at + ``'http://{JUPYTER_HOST}:{port}'``. + + Use ``JUPYTER_PROXY`` to leverage ``jupyter-server-proxy`` to + proxy the tile serving port through Jupyter's authenticated web + interface. This is useful in Docker and cloud JupyterHub + environments. You can set the environment variable + ``LARGE_IMAGE_JUPYTER_PROXY`` to control the default value of + ``JUPYTER_PROXY``. If ``JUPYTER_PROXY`` is set to ``True``, the + default will be ``'/proxy/`` which will work for most Docker + Jupyter configurations. If in a cloud JupyterHub environment, + this will get a bit more nuanced as the + ``JUPYTERHUB_SERVICE_PREFIX`` may need to prefix the + ``'/proxy/'``. + + To programmatically set these values: + + .. code:: + + from large_image.tilesource.jupyter import IPyLeafletMixin + + # Only set one of these values + + # Use a custom domain (avoids port proxying) + IPyLeafletMixin.JUPYTER_HOST = 'mydomain' + + # Proxy in a standard JupyterLab environment + IPyLeafletMixin.JUPYTER_PROXY = True # defaults to `/proxy/` + + # Proxy in a cloud JupyterHub environment + IPyLeafletMixin.JUPYTER_PROXY = '/jupyter/user/username/proxy/' + # See if ``JUPYTERHUB_SERVICE_PREFIX`` is in the environment + # variables to improve this + + """ + + JUPYTER_HOST = '127.0.0.1' + JUPYTER_PROXY = os.environ.get('LARGE_IMAGE_JUPYTER_PROXY', False) + + _jupyter_server_manager: Any + + def __init__(self, *args, **kwargs) -> None: + self._jupyter_server_manager = None + self._map = Map() + if ipyleafletPresent: + self.to_map = self._map.to_map + self.from_map = self._map.from_map + +
+[docs] + def as_leaflet_layer(self, **kwargs) -> Any: + # NOTE: `as_leaflet_layer` is supported by ipyleaflet.Map.add + + if self._jupyter_server_manager is None: + # Must relaunch to ensure style updates work + self._jupyter_server_manager = launch_tile_server(self) + else: + # Must update the source on the manager in case the previous reference is bad + self._jupyter_server_manager.tile_source = self + + port = self._jupyter_server_manager.port + + if self.JUPYTER_PROXY: + if isinstance(self.JUPYTER_PROXY, str): + base_url = f'{self.JUPYTER_PROXY.rstrip("/")}/{port}' + else: + base_url = f'/proxy/{port}' + else: + base_url = f'http://{self.JUPYTER_HOST}:{port}' + + # Use repr in URL params to prevent caching across sources/styles + endpoint = f'tile?z={{z}}&x={{x}}&y={{y}}&encoding=png&repr={self.__repr__()}' + return self._map.make_layer( + self.metadata, # type: ignore[attr-defined] + f'{base_url}/{endpoint}')
+ + + # Only make _ipython_display_ available if ipyleaflet is installed + if ipyleafletPresent: + + def _ipython_display_(self) -> Any: + from IPython.display import display + + t = self.as_leaflet_layer() + + return display(self._map.make_map( + self.metadata, t, self.getCenter(srs='EPSG:4326'))) # type: ignore[attr-defined] + + @property + def iplmap(self) -> Any: + """ + If using ipyleaflets, get access to the map object. + """ + return self._map.map
+ + + +
+[docs] +class Map: + """ + An IPyLeafletMap representation of a large image. + """ + + def __init__( + self, *, ts: Optional[IPyLeafletMixin] = None, + metadata: Optional[Dict] = None, url: Optional[str] = None, + gc: Optional[Any] = None, id: Optional[str] = None, + resource: Optional[str] = None) -> None: + """ + Specify the large image to be used with the IPyLeaflet Map. One of (a) + a tile source, (b) metadata dictionary and tile url, (c) girder client + and item or file id, or (d) girder client and resource path must be + specified. + + :param ts: a TileSource. + :param metadata: a metadata dictionary as returned by a tile source or + a girder item/{id}/tiles endpoint. + :param url: a slippy map template url to fetch tiles (e.g., + .../item/{id}/tiles/zxy/{z}/{x}/{y}?params=...) + :param gc: an authenticated girder client. + :param id: an item id that exists on the girder client. + :param resource: a girder resource path of an item or file that exists + on the girder client. + """ + self._layer = self._map = self._metadata = self._frame_slider = None + self._ts = ts + if (not url or not metadata) and gc and (id or resource): + fileId = None + if id is None: + entry = gc.get('resource/lookup', parameters={'path': resource}) + if entry: + if entry.get('_modelType') == 'file': + fileId = entry['_id'] + id = entry['itemId'] if entry.get('_modelType') == 'file' else entry['_id'] + if id: + try: + metadata = gc.get(f'item/{id}/tiles') + except Exception: + pass + if metadata: + url = gc.urlBase + f'item/{id}/tiles' + '/zxy/{z}/{x}/{y}' + if metadata.get('geospatial'): + suffix = '?projection=EPSG:3857&encoding=PNG' + metadata = gc.get(f'item/{id}/tiles' + suffix) + if (cast(Dict, metadata).get('geospatial') and + cast(Dict, metadata).get('projection')): + url += suffix # type: ignore[operator] + self._id = id + else: + self._ts = self._get_temp_source(gc, cast(str, fileId or id)) + if url and metadata: + self._metadata = metadata + self._url = url + self._layer = self.make_layer(metadata, url) + self._map = self.make_map(metadata) + + if ipyleafletPresent: + def _ipython_display_(self) -> Any: + from IPython.display import display + + if self._map: + return display(self._map) + if self._ts: + t = self._ts.as_leaflet_layer() + return display(self._ts._map.make_map( + self._ts.metadata, # type: ignore[attr-defined] + t, + self._ts.getCenter(srs='EPSG:4326'))) # type: ignore[attr-defined] + + def _get_temp_source(self, gc: Any, id: str) -> IPyLeafletMixin: + """ + If the server isn't large_image enabled, download the file to view it. + """ + import tempfile + + import large_image + + try: + item = gc.get(f'item/{id}') + except Exception: + item = None + if not item: + file = gc.get(f'file/{id}') + else: + file = gc.get(f'item/{id}/files', parameters={'limit': 1})[0] + self._tempfile = tempfile.NamedTemporaryFile(suffix='.' + file['name'].split('.', 1)[-1]) + gc.downloadFile(file['_id'], self._tempfile) + return large_image.open(self._tempfile.name) + +
+[docs] + def make_layer(self, metadata: Dict, url: str, **kwargs) -> Any: + """ + Create an ipyleaflet tile layer given large_image metadata and a tile + url. + """ + from ipyleaflet import TileLayer + + self._geospatial = metadata.get('geospatial') and metadata.get('projection') + if 'bounds' not in kwargs and not self._geospatial: + kwargs = kwargs.copy() + kwargs['bounds'] = [[0, 0], [metadata['sizeY'], metadata['sizeX']]] + layer = TileLayer( + url=url, + # attribution='Tiles served with large-image', + min_zoom=0, + max_native_zoom=metadata['levels'] - 1, + max_zoom=20, + tile_size=metadata['tileWidth'], + **kwargs, + ) + self._layer = layer + if not self._metadata: + self._metadata = metadata + return layer
+ + +
+[docs] + def make_map( + self, metadata: Dict, layer: Optional[Any] = None, + center: Optional[Tuple[float, float]] = None) -> Any: + """ + Create an ipyleaflet map given large_image metadata, an optional + ipyleaflet layer, and the center of the tile source. + """ + from ipyleaflet import Map, basemaps, projections + from ipywidgets import IntSlider, VBox + + try: + default_zoom = metadata['levels'] - metadata['sourceLevels'] + except KeyError: + default_zoom = 0 + + self._geospatial = metadata.get('geospatial') and metadata.get('projection') + + if self._geospatial: + # TODO: better handle other projections + crs = projections.EPSG3857 + else: + crs = dict( + name='PixelSpace', + custom=True, + # Why does this need to be 256? + resolutions=[2 ** (metadata['levels'] - 1 - l) for l in range(20)], + + # This works but has x and y reversed + proj4def='+proj=longlat +axis=esu', + bounds=[[0, 0], [metadata['sizeY'], metadata['sizeX']]], + # Why is origin X, Y but bounds Y, X? + origin=[0, metadata['sizeY']], + + # This almost works to fix the x, y reversal, but + # - bounds are weird and other issues occur + # proj4def='+proj=longlat +axis=seu', + # bounds=[[-metadata['sizeX'],-metadata['sizeY']], + # [metadata['sizeX'],metadata['sizeY']]], + # origin=[0,0], + ) + layer = layer or self._layer + + if center is None: + if 'bounds' in metadata and 'projection' in metadata: + import pyproj + + bounds = metadata['bounds'] + center = ( + (bounds['ymax'] + bounds['ymin']) / 2, + (bounds['xmax'] + bounds['xmin']) / 2, + ) + transf = pyproj.Transformer.from_crs( + metadata['projection'], 'EPSG:4326', always_xy=True) + center = tuple(transf.transform(center[1], center[0])[::-1]) + else: + center = (metadata['sizeY'] / 2, metadata['sizeX'] / 2) + + children: List[Any] = [] + frames = metadata.get('frames') + if frames is not None: + self._frame_slider = IntSlider( + value=0, + min=0, + max=len(frames) - 1, + description='Frame:', + ) + if self._frame_slider: + self._frame_slider.observe(self.update_frame, names='value') + children.append(self._frame_slider) + + m = Map( + crs=crs, + basemap=basemaps.OpenStreetMap.Mapnik if self._geospatial else layer, + center=center, + zoom=default_zoom, + max_zoom=metadata['levels'] + 1, + min_zoom=0, + scroll_wheel_zoom=True, + dragging=True, + attribution_control=False, + ) + if self._geospatial: + m.add_layer(layer) + self._map = m + children.append(m) + + return VBox(children)
+ + + @property + def layer(self) -> Any: + return self._layer + + @property + def map(self) -> Any: + return self._map + + @property + def metadata(self) -> JSONDict: + return JSONDict(self._metadata) + + @property + def id(self) -> Optional[str]: + return getattr(self, '_id', None) + +
+[docs] + def to_map(self, coordinate: Union[List[float], Tuple[float, float]]) -> Tuple[float, float]: + """ + Convert a coordinate from the image or projected image space to the map + space. + + :param coordinate: a two-tuple that is x, y in pixel space or x, y in + image projection space. + :returns: a two-tuple that is in the map space coordinates. + """ + x, y = coordinate[:2] + if not self._metadata: + return y, x + if self._geospatial: + import pyproj + + transf = pyproj.Transformer.from_crs( + self._metadata['projection'], 'EPSG:4326', always_xy=True) + return tuple(transf.transform(x, y)[::-1]) + return self._metadata['sizeY'] - y, x
+ + +
+[docs] + def from_map(self, coordinate: Union[List[float], Tuple[float, float]]) -> Tuple[float, float]: + """ + :param coordinate: a two-tuple that is in the map space coordinates. + :returns: a two-tuple that is x, y in pixel space or x, y in image + projection space. + """ + y, x = coordinate[:2] + if not self._metadata: + return x, y + if self._geospatial: + import pyproj + + transf = pyproj.Transformer.from_crs( + 'EPSG:4326', self._metadata['projection'], always_xy=True) + return transf.transform(x, y) + return x, self._metadata['sizeY'] - y
+ + +
+[docs] + def update_frame(self, event, **kwargs): + frame = int(event.get('new')) + if self._layer: + if 'frame=' in self._layer.url: + self._layer.url = re.sub(r'frame=(\d+)', f'frame={frame}', self._layer.url) + else: + if '?' in self._layer.url: + self._layer.url = self._layer.url.replace('?', f'?frame={frame}&') + else: + self._layer.url += f'?frame={frame}' + + self._layer.redraw()
+
+ + + +
+[docs] +def launch_tile_server(tile_source: IPyLeafletMixin, port: int = 0) -> Any: + import tornado.httpserver + import tornado.netutil + import tornado.web + + class RequestManager: + def __init__(self, tile_source: IPyLeafletMixin) -> None: + self._tile_source_ = weakref.ref(tile_source) + self._ports = () + + @property + def tile_source(self) -> IPyLeafletMixin: + return cast(IPyLeafletMixin, self._tile_source_()) + + @tile_source.setter + def tile_source(self, source: IPyLeafletMixin) -> None: + self._tile_source_ = weakref.ref(source) + + @property + def ports(self) -> Tuple[int, ...]: + return self._ports + + @property + def port(self) -> int: + return self.ports[0] + + manager = RequestManager(tile_source) + # NOTE: set `ports` manually after launching server + + class TileSourceMetadataHandler(tornado.web.RequestHandler): + """REST endpoint to get image metadata.""" + + def get(self) -> None: + self.write(json.dumps(manager.tile_source.metadata)) # type: ignore[attr-defined] + self.set_header('Content-Type', 'application/json') + + class TileSourceTileHandler(tornado.web.RequestHandler): + """REST endpoint to serve tiles from image in slippy maps standard.""" + + def get(self) -> None: + x = int(self.get_argument('x')) + y = int(self.get_argument('y')) + z = int(self.get_argument('z')) + frame = int(self.get_argument('frame', default='0')) + encoding = self.get_argument('encoding', 'PNG') + try: + tile_binary = manager.tile_source.getTile( # type: ignore[attr-defined] + x, y, z, encoding=encoding, frame=frame) + except TileSourceXYZRangeError as e: + self.clear() + self.set_status(404) + self.finish(f'<html><body>{e}</body></html>') + else: + self.write(tile_binary) + self.set_header('Content-Type', 'image/png') + + app = tornado.web.Application([ + (r'/metadata', TileSourceMetadataHandler), + (r'/tile', TileSourceTileHandler), + ]) + sockets = tornado.netutil.bind_sockets(port, '') + server = tornado.httpserver.HTTPServer(app) + server.add_sockets(sockets) + + manager._ports = tuple(s.getsockname()[1] for s in sockets) + return manager
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource/resample.html b/_modules/large_image/tilesource/resample.html new file mode 100644 index 000000000..da3c083e6 --- /dev/null +++ b/_modules/large_image/tilesource/resample.html @@ -0,0 +1,260 @@ + + + + + + large_image.tilesource.resample — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource.resample

+from enum import Enum
+from typing import Dict
+
+import numpy as np
+from PIL import Image
+
+
+
+[docs] +class ResampleMethod(Enum): + PIL_NEAREST = Image.Resampling.NEAREST # 0 + PIL_LANCZOS = Image.Resampling.LANCZOS # 1 + PIL_BILINEAR = Image.Resampling.BILINEAR # 2 + PIL_BICUBIC = Image.Resampling.BICUBIC # 3 + PIL_BOX = Image.Resampling.BOX # 4 + PIL_HAMMING = Image.Resampling.HAMMING # 5 + PIL_MAX_ENUM = 5 + NP_MEAN = 6 + NP_MEDIAN = 7 + NP_MODE = 8 + NP_MAX = 9 + NP_MIN = 10 + NP_NEAREST = 11 + NP_MAX_COLOR = 12 + NP_MIN_COLOR = 13
+ + + +
+[docs] +def pilResize( + tile: np.ndarray, + new_shape: Dict, + resample_method: ResampleMethod, +) -> np.ndarray: + # Only NEAREST works for 16 bit images + img = Image.fromarray(tile) + resized_img = img.resize( + (new_shape['width'], new_shape['height']), + resample=resample_method.value, + ) + result = np.array(resized_img).astype(tile.dtype) + return result
+ + + +
+[docs] +def numpyResize( + tile: np.ndarray, + new_shape: Dict, + resample_method: ResampleMethod, +) -> np.ndarray: + if resample_method == ResampleMethod.NP_NEAREST: + return tile[::2, ::2] + else: + if tile.shape[0] % 2 != 0: + tile = np.append(tile, np.expand_dims(tile[-1], axis=0), axis=0) + if tile.shape[1] % 2 != 0: + tile = np.append(tile, np.expand_dims(tile[:, -1], axis=1), axis=1) + + pixel_selection = None + subarrays = np.asarray( + [ + tile[0::2, 0::2], + tile[1::2, 0::2], + tile[0::2, 1::2], + tile[1::2, 1::2], + ], + ) + + if resample_method == ResampleMethod.NP_MEAN: + return np.mean(subarrays, axis=0).astype(tile.dtype) + elif resample_method == ResampleMethod.NP_MEDIAN: + return np.median(subarrays, axis=0).astype(tile.dtype) + elif resample_method == ResampleMethod.NP_MAX: + return np.max(subarrays, axis=0).astype(tile.dtype) + elif resample_method == ResampleMethod.NP_MIN: + return np.min(subarrays, axis=0).astype(tile.dtype) + elif resample_method == ResampleMethod.NP_MAX_COLOR: + summed = np.sum(subarrays, axis=3) + pixel_selection = np.argmax(summed, axis=0) + elif resample_method == ResampleMethod.NP_MIN_COLOR: + summed = np.sum(subarrays, axis=3) + pixel_selection = np.argmin(summed, axis=0) + elif resample_method == ResampleMethod.NP_MODE: + # if a pixel occurs twice in a set of four, it is a mode + # if no mode, default to pixel 0. check for minimal matches 1=2, 1=3, 2=3 + pixel_selection = np.where( + ( + (subarrays[1] == subarrays[2]).all(axis=2) | + (subarrays[1] == subarrays[3]).all(axis=2) + ), + 1, np.where( + (subarrays[2] == subarrays[3]).all(axis=2), + 2, 0, + ), + ) + + if pixel_selection is not None: + if len(tile.shape) > 2: + pixel_selection = np.expand_dims(pixel_selection, axis=2) + pixel_selection = np.repeat(pixel_selection, tile.shape[2], axis=2) + return np.choose(pixel_selection, subarrays).astype(tile.dtype) + else: + msg = f'Unknown resample method {resample_method}.' + raise ValueError(msg)
+ + + +
+[docs] +def downsampleTileHalfRes( + tile: np.ndarray, + resample_method: ResampleMethod, +) -> np.ndarray: + new_shape = { + 'height': (tile.shape[0] + 1) // 2, + 'width': (tile.shape[1] + 1) // 2, + 'bands': 1, + } + if len(tile.shape) > 2: + new_shape['bands'] = tile.shape[-1] + if resample_method.value <= ResampleMethod.PIL_MAX_ENUM.value: + if new_shape['bands'] > 4: + result = np.empty( + (new_shape['height'], new_shape['width'], new_shape['bands']), + dtype=tile.dtype, + ) + for band_index in range(new_shape['bands']): + result[(..., band_index)] = pilResize( + tile[(..., band_index)], + new_shape, + resample_method, + ) + return result + else: + return pilResize(tile, new_shape, resample_method) + else: + return numpyResize(tile, new_shape, resample_method)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource/stylefuncs.html b/_modules/large_image/tilesource/stylefuncs.html new file mode 100644 index 000000000..57412fad1 --- /dev/null +++ b/_modules/large_image/tilesource/stylefuncs.html @@ -0,0 +1,230 @@ + + + + + + large_image.tilesource.stylefuncs — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource.stylefuncs

+# This module contains functions for use in styles
+
+from types import SimpleNamespace
+from typing import List, Optional, Tuple, Union
+
+import numpy as np
+
+from .utilities import _imageToNumpy, _imageToPIL
+
+
+
+[docs] +def maskPixelValues( + image: np.ndarray, context: SimpleNamespace, + values: List[Union[int, List[int], Tuple[int, ...]]], + negative: Optional[int] = None, positive: Optional[int] = None) -> np.ndarray: + """ + This is a style utility function that returns a black-and-white 8-bit image + where the image is white if the pixel of the source image is in a list of + values and black otherwise. The values is a list where each entry can + either be a tuple the same length as the band dimension of the output image + or a single value which is handled as 0xBBGGRR. + + :param image: a numpy array of Y, X, Bands. + :param context: the style context. context.image is the source image + :param values: an array of values, each of which is either an array of the + same number of bands as the source image or a single value of the form + 0xBBGGRR assuming uint8 data. + :param negative: None to use [0, 0, 0, 255], or an RGBA uint8 value for + pixels not in the value list. + :param positive: None to use [255, 255, 255, 0], or an RGBA uint8 value for + pixels in the value list. + :returns: an RGBA numpy image which is exactly black or transparent white. + """ + src = context.image + mask = np.full(src.shape[:2], False) + for val in values: + vallist: List[float] + if not isinstance(val, (list, tuple)): + if src.shape[-1] == 1: + vallist = [val] + else: + vallist = [val % 256, val // 256 % 256, val // 65536 % 256] + else: + vallist = list(val) + vallist = (vallist + [255] * src.shape[2])[:src.shape[2]] + match = np.array(vallist) + mask = mask | (src == match).all(axis=-1) + image[mask != True] = negative or [0, 0, 0, 255] # noqa E712 + image[mask] = positive or [255, 255, 255, 0] + image = image.astype(np.uint8) + return image
+ + + +
+[docs] +def medianFilter( + image: np.ndarray, context: Optional[SimpleNamespace] = None, + kernel: int = 5, weight: float = 1.0) -> np.ndarray: + """ + This is a style utility function that applies a median rank filter to the + image to sharpen it. + + :param image: a numpy array of Y, X, Bands. + :param context: the style context. context.image is the source image + :param kernel: the filter kernel size. + :param weight: the weight of the difference between the image and the + filtered image that is used to add into the image. 0 is no effect/ + :returns: an numpy image which is the filtered version of the source. + """ + import PIL.ImageFilter + + filt = PIL.ImageFilter.MedianFilter(kernel) + if len(image.shape) != 3: + pilimg = _imageToPIL(image) + elif image.shape[2] >= 3: + pilimg = _imageToPIL(image[:, :, :3]) + else: + pilimg = _imageToPIL(image[:, :, :1]) + fimg = _imageToNumpy(pilimg.filter(filt))[0] + mul: float = 0 + clip = 0 + if image.dtype == np.uint8 or ( + image.dtype.kind == 'f' and 1 < np.max(image) < 256 and np.min(image) >= 0): + mul = 1 + clip = 255 + elif image.dtype == np.uint16 or ( + image.dtype.kind == 'f' and 1 < np.max(image) < 65536 and np.min(image) >= 0): + mul = 257 + clip = 65535 + elif image.dtype == np.uint32: + mul = (2 ** 32 - 1) / 255 + clip = 2 ** 32 - 1 + elif image.dtype.kind == 'f': + mul = 1 + if mul: + pimg: np.ndarray = image.astype(float) + if len(pimg.shape) == 2: + pimg = np.resize(pimg, (pimg.shape[0], pimg.shape[1], 1)) + pimg = pimg[:, :, :fimg.shape[2]] # type: ignore[index,misc] + dimg = (pimg - fimg.astype(float) * mul) * weight + pimg = pimg[:, :, :fimg.shape[2]] + dimg # type: ignore[index,misc] + if clip: + pimg = pimg.clip(0, clip) + if len(image.shape) != 3: + image[:, :] = np.resize(pimg.astype(image.dtype), (pimg.shape[0], pimg.shape[1])) + else: + image[:, :, :fimg.shape[2]] = pimg.astype(image.dtype) + return image
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource/tiledict.html b/_modules/large_image/tilesource/tiledict.html new file mode 100644 index 000000000..5e17fddab --- /dev/null +++ b/_modules/large_image/tilesource/tiledict.html @@ -0,0 +1,380 @@ + + + + + + large_image.tilesource.tiledict — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource.tiledict

+from typing import Any, Dict, Optional, Tuple, cast
+
+import numpy as np
+import PIL
+import PIL.Image
+import PIL.ImageColor
+import PIL.ImageDraw
+
+from .. import exceptions
+from ..constants import TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY, TILE_FORMAT_PIL
+from .utilities import _encodeImage, _imageToNumpy, _imageToPIL
+
+
+
+[docs] +class LazyTileDict(dict): + """ + Tiles returned from the tile iterator and dictionaries of information with + actual image data in the 'tile' key and the format in the 'format' key. + Since some applications need information about the tile but don't need the + image data, these two values are lazily computed. The LazyTileDict can be + treated like a regular dictionary, except that when either of those two + keys are first accessed, they will cause the image to be loaded and + possibly converted to a PIL image and cropped. + + Unless setFormat is called on the tile, tile images may always be returned + as PIL images. + """ + + def __init__(self, tileInfo: Dict[str, Any], *args, **kwargs) -> None: + """ + Create a LazyTileDict dictionary where there is enough information to + load the tile image. ang and kwargs are as for the dict() class. + + :param tileInfo: a dictionary of x, y, level, format, encoding, crop, + and source, used for fetching the tile image. + """ + self.x = tileInfo['x'] + self.y = tileInfo['y'] + self.frame = tileInfo.get('frame') + self.level = tileInfo['level'] + self.format = tileInfo['format'] + self.encoding = tileInfo['encoding'] + self.crop = tileInfo['crop'] + self.source = tileInfo['source'] + self.resample = tileInfo.get('resample', False) + self.requestedScale = tileInfo.get('requestedScale') + self.metadata = cast(Dict[str, Any], tileInfo.get('metadata')) + self.retile = tileInfo.get('retile') and self.metadata + + self.deferredKeys = ('tile', 'format') + self.alwaysAllowPIL = True + self.imageKwargs: Dict[str, Any] = {} + self.loaded = False + super().__init__(*args, **kwargs) + # We set this initially so that they are listed in known keys using the + # native dictionary methods + self['tile'] = None + self['format'] = None + self.width = self['width'] + self.height = self['height'] + +
+[docs] + def setFormat( + self, format: Tuple[str, ...], resample: bool = False, + imageKwargs: Optional[Dict[str, Any]] = None) -> None: + """ + Set a more restrictive output format for a tile, possibly also resizing + it via resampling. If this is not called, the tile may either be + returned as one of the specified formats or as a PIL image. + + :param format: a tuple or list of allowed formats. Formats are members + of TILE_FORMAT_*. This will avoid converting images if they are + in the desired output encoding (regardless of subparameters). + :param resample: if not False or None, allow resampling. Once turned + on, this cannot be turned off on the tile. + :param imageKwargs: additional parameters that should be passed to + _encodeImage. + """ + # If any parameters are changed, mark the tile as not loaded, so that + # referring to a deferredKey will reload the image. + self.alwaysAllowPIL = False + if format is not None and format != self.format: + self.format = format + self.loaded = False + if (resample not in (False, None) and not self.resample and + self.requestedScale and round(self.requestedScale, 2) != 1.0): + self.resample = resample + self['scaled'] = 1.0 / self.requestedScale + self['tile_x'] = self.get('tile_x', self['x']) + self['tile_y'] = self.get('tile_y', self['y']) + self['tile_width'] = self.get('tile_width', self.width) + self['tile_height'] = self.get('tile_width', self.height) + if self.get('magnification', None): + self['tile_magnification'] = self.get('tile_magnification', self['magnification']) + self['tile_mm_x'] = self.get('mm_x') + self['tile_mm_y'] = self.get('mm_y') + self['x'] = float(self['tile_x']) + self['y'] = float(self['tile_y']) + # Add provisional width and height + if self.resample not in (False, None) and self.requestedScale: + self['width'] = max(1, int( + self['tile_width'] / self.requestedScale)) + self['height'] = max(1, int( + self['tile_height'] / self.requestedScale)) + if self.get('tile_magnification', None): + self['magnification'] = self['tile_magnification'] / self.requestedScale + if self.get('tile_mm_x', None): + self['mm_x'] = self['tile_mm_x'] * self.requestedScale + if self.get('tile_mm_y', None): + self['mm_y'] = self['tile_mm_y'] * self.requestedScale + # If we can resample the tile, many parameters may change once the + # image is loaded. Don't include width and height in this list; + # the provisional values are sufficient. + self.deferredKeys = ('tile', 'format') + self.loaded = False + if imageKwargs is not None: + self.imageKwargs = imageKwargs + self.loaded = False
+ + + def _retileTile(self) -> np.ndarray: + """ + Given the tile information, create a numpy array and merge multiple + tiles together to form a tile of a different size. + """ + tileWidth = self.metadata['tileWidth'] + tileHeight = self.metadata['tileHeight'] + level = self.level + frame = self.frame + width = self.width + height = self.height + tx = self['x'] + ty = self['y'] + + retile = None + xmin = int(max(0, tx // tileWidth)) + xmax = int((tx + width - 1) // tileWidth + 1) + ymin = int(max(0, ty // tileHeight)) + ymax = int((ty + height - 1) // tileHeight + 1) + for y in range(ymin, ymax): + for x in range(xmin, xmax): + tileData = self.source.getTile( + x, y, level, + numpyAllowed='always', sparseFallback=True, frame=frame) + if not isinstance(tileData, np.ndarray) or len(tileData.shape) != 3: + tileData, _ = _imageToNumpy(tileData) + x0 = int(x * tileWidth - tx) + y0 = int(y * tileHeight - ty) + if x0 < 0: + tileData = tileData[:, -x0:] + x0 = 0 + if y0 < 0: + tileData = tileData[-y0:, :] + y0 = 0 + tw = min(tileData.shape[1], width - x0) + th = min(tileData.shape[0], height - y0) + if retile is None: + retile = np.empty((height, width, tileData.shape[2]), dtype=tileData.dtype) + elif tileData.shape[2] < retile.shape[2]: + retile = retile[:, :, :tileData.shape[2]] + retile[y0:y0 + th, x0:x0 + tw] = tileData[ + :th, :tw, :retile.shape[2]] # type: ignore[misc] + return cast(np.ndarray, retile) + + def __getitem__(self, key: str, *args, **kwargs) -> Any: + """ + If this is the first time either the tile or format key is requested, + load the tile image data. Otherwise, just return the internal + dictionary result. + + See the base dict class for function details. + """ + if not self.loaded and key in self.deferredKeys: + # Flag this immediately to avoid recursion if we refer to the + # tile's own values. + self.loaded = True + + if not self.retile: + tileData = self.source.getTile( + self.x, self.y, self.level, + pilImageAllowed=True, + numpyAllowed='always' if TILE_FORMAT_NUMPY in self.format else True, + sparseFallback=True, frame=self.frame) + if self.crop: + tileData, _ = _imageToNumpy(tileData) + tileData = tileData[self.crop[1]:self.crop[3], self.crop[0]:self.crop[2]] + else: + tileData = self._retileTile() + + pilData = None + # resample if needed + if self.resample not in (False, None) and self.requestedScale: + pilData = _imageToPIL(tileData) + + self['width'] = max(1, int( + pilData.size[0] / self.requestedScale)) + self['height'] = max(1, int( + pilData.size[1] / self.requestedScale)) + pilData = tileData = pilData.resize( + (self['width'], self['height']), + resample=getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS + if self.resample is True else self.resample) + + tileFormat = (TILE_FORMAT_PIL if isinstance(tileData, PIL.Image.Image) + else (TILE_FORMAT_NUMPY if isinstance(tileData, np.ndarray) + else TILE_FORMAT_IMAGE)) + tileEncoding = None if tileFormat != TILE_FORMAT_IMAGE else ( + 'JPEG' if tileData[:3] == b'\xff\xd8\xff' else + 'PNG' if tileData[:4] == b'\x89PNG' else + 'TIFF' if tileData[:4] == b'II\x2a\x00' else + None) + # Reformat the image if required + if (not self.alwaysAllowPIL or + (TILE_FORMAT_NUMPY in self.format and isinstance(tileData, np.ndarray))): + if (tileFormat in self.format and (tileFormat != TILE_FORMAT_IMAGE or ( + tileEncoding and + tileEncoding == self.imageKwargs.get('encoding', self.encoding)))): + # already in an acceptable format + pass + elif TILE_FORMAT_NUMPY in self.format: + tileData, _ = _imageToNumpy(tileData) + tileFormat = TILE_FORMAT_NUMPY + elif TILE_FORMAT_PIL in self.format: + tileData = pilData if pilData is not None else _imageToPIL(tileData) + tileFormat = TILE_FORMAT_PIL + elif TILE_FORMAT_IMAGE in self.format: + tileData, mimeType = _encodeImage( + tileData, **self.imageKwargs) + tileFormat = TILE_FORMAT_IMAGE + if tileFormat not in self.format: + raise exceptions.TileSourceError( + 'Cannot yield tiles in desired format %r' % ( + self.format, )) + elif (TILE_FORMAT_PIL not in self.format and TILE_FORMAT_NUMPY in self.format and + not isinstance(tileData, PIL.Image.Image)): + tileData, _ = _imageToNumpy(tileData) + tileFormat = TILE_FORMAT_NUMPY + else: + tileData = pilData if pilData is not None else _imageToPIL(tileData) + tileFormat = TILE_FORMAT_PIL + + self['tile'] = tileData + self['format'] = tileFormat + return super().__getitem__(key, *args, **kwargs) + +
+[docs] + def release(self) -> None: + """ + If the tile has been loaded, unload it. It can be loaded again. This + is useful if you want to keep tiles available in memory but not their + actual tile data. + """ + if self.loaded: + self.loaded = False + for key in self.deferredKeys: + self[key] = None
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource/tileiterator.html b/_modules/large_image/tilesource/tileiterator.html new file mode 100644 index 000000000..afdb21832 --- /dev/null +++ b/_modules/large_image/tilesource/tileiterator.html @@ -0,0 +1,666 @@ + + + + + + large_image.tilesource.tileiterator — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource.tileiterator

+import math
+from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, Tuple, Union, cast
+
+from ..constants import TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY, TILE_FORMAT_PIL, TileOutputMimeTypes
+from . import utilities
+from .tiledict import LazyTileDict
+
+if TYPE_CHECKING:
+    from .. import tilesource
+
+
+
+[docs] +class TileIterator: + """ + A tile iterator on a TileSource. Details about the iterator can be read + via the `info` attribute on the iterator. + """ + + def __init__( + self, source: 'tilesource.TileSource', + format: Union[str, Tuple[str]] = (TILE_FORMAT_NUMPY, ), + resample: Optional[bool] = True, **kwargs) -> None: + self.source = source + self._kwargs = kwargs + if not isinstance(format, tuple): + format = (format, ) + if TILE_FORMAT_IMAGE in format: + encoding = kwargs.get('encoding') + if encoding not in TileOutputMimeTypes: + raise ValueError('Invalid encoding "%s"' % encoding) + self.format = format + self.resample = resample + iterFormat = format if resample in (False, None) else (TILE_FORMAT_PIL, ) + self.info = self._tileIteratorInfo(format=iterFormat, resample=resample, **kwargs) + if self.info is None: + self._iter = None + return + if resample in (False, None) or round(self.info['requestedScale'], 2) == 1.0: + self.resample = False + self._iter = self._tileIterator(self.info) + + def __iter__(self) -> Iterator[LazyTileDict]: + return self + + def __next__(self) -> LazyTileDict: + if self._iter is None: + raise StopIteration + try: + tile = next(self._iter) + tile.setFormat(self.format, bool(self.resample), self._kwargs) + return tile + except StopIteration: + raise + + def __repr__(self) -> str: + repr = f'TileIterator<{self.source}' + if self.info: + repr += ( + f': {self.info["output"]["width"]} x {self.info["output"]["height"]}' + f'; tiles: {self.info["tile_count"]}' + f'; region: {self.info["region"]}') + if self.info['frame'] is not None: + repr += f'; frame: {self.info["frame"]}>' + repr += '>' + return repr + + def _repr_json_(self) -> Dict: + if self.info: + return self.info + return {} + + def _tileIteratorInfo(self, **kwargs) -> Optional[Dict[str, Any]]: # noqa + """ + Get information necessary to construct a tile iterator. + If one of width or height is specified, the other is determined by + preserving aspect ratio. If both are specified, the result may not be + that size, as aspect ratio is always preserved. If neither are + specified, magnification, mm_x, and/or mm_y are used to determine the + size. If none of those are specified, the original maximum resolution + is returned. + + :param format: a tuple of allowed formats. Formats are members of + TILE_FORMAT_*. This will avoid converting images if they are + in the desired output encoding (regardless of subparameters). + Otherwise, TILE_FORMAT_NUMPY is returned. + :param region: a dictionary of optional values which specify the part + of the image to process. + + :left: the left edge (inclusive) of the region to process. + :top: the top edge (inclusive) of the region to process. + :right: the right edge (exclusive) of the region to process. + :bottom: the bottom edge (exclusive) of the region to process. + :width: the width of the region to process. + :height: the height of the region to process. + :units: either 'base_pixels' (default), 'pixels', 'mm', or + 'fraction'. base_pixels are in maximum resolution pixels. + pixels is in the specified magnification pixels. mm is in the + specified magnification scale. fraction is a scale of 0 to 1. + pixels and mm are only available if the magnification and mm + per pixel are defined for the image. + :unitsWH: if not specified, this is the same as `units`. + Otherwise, these units will be used for the width and height if + specified. + + :param output: a dictionary of optional values which specify the size + of the output. + + :maxWidth: maximum width in pixels. + :maxHeight: maximum height in pixels. + + :param scale: a dictionary of optional values which specify the scale + of the region and / or output. This applies to region if + pixels or mm are used for units. It applies to output if + neither output maxWidth nor maxHeight is specified. + + :magnification: the magnification ratio. + :mm_x: the horizontal size of a pixel in millimeters. + :mm_y: the vertical size of a pixel in millimeters. + :exact: if True, only a level that matches exactly will be + returned. This is only applied if magnification, mm_x, or mm_y + is used. + + :param tile_position: if present, either a number to only yield the + (tile_position)th tile [0 to (xmax - min) * (ymax - ymin)) that the + iterator would yield, or a dictionary of {region_x, region_y} to + yield that tile, where 0, 0 is the first tile yielded, and + xmax - xmin - 1, ymax - ymin - 1 is the last tile yielded, or a + dictionary of {level_x, level_y} to yield that specific tile if it + is in the region. + :param tile_size: if present, retile the output to the specified tile + size. If only width or only height is specified, the resultant + tiles will be square. This is a dictionary containing at least + one of: + + :width: the desired tile width. + :height: the desired tile height. + + :param tile_overlap: if present, retile the output adding a symmetric + overlap to the tiles. If either x or y is not specified, it + defaults to zero. The overlap does not change the tile size, + only the stride of the tiles. This is a dictionary containing: + + :x: the horizontal overlap in pixels. + :y: the vertical overlap in pixels. + :edges: if True, then the edge tiles will exclude the overlap + distance. If unset or False, the edge tiles are full size. + + :param tile_offset: if present, adjust tile positions so that the + corner of one tile is at the specified location. + + :left: the left offset in pixels. + :top: the top offset in pixels. + :auto: a boolean, if True, automatically set the offset to align + with the region's left and top. + + :param kwargs: optional arguments. Some options are encoding, + jpegQuality, jpegSubsampling, tiffCompression, frame. + :returns: a dictionary of information needed for the tile iterator. + This is None if no tiles will be returned. Otherwise, this + contains: + + :region: a dictionary of the source region information: + + :width, height: the total output of the iterator in pixels. + This may be larger than the requested resolution (given by + output width and output height) if there isn't an exact + match between the requested resolution and available native + tiles. + :left, top, right, bottom: the coordinates within the image of + the region returned in the level pixel space. + + :xmin, ymin, xmax, ymax: the tiles that will be included during the + iteration: [xmin, xmax) and [ymin, ymax). + :mode: either 'RGB' or 'RGBA'. This determines the color space + used for tiles. + :level: the tile level used for iteration. + :metadata: tile source metadata (from getMetadata) + :output: a dictionary of the output resolution information. + + :width, height: the requested output resolution in pixels. If + this is different that region width and region height, then + the original request was asking for a different scale than + is being delivered. + + :frame: the frame value for the base image. + :format: a tuple of allowed output formats. + :encoding: if the output format is TILE_FORMAT_IMAGE, the desired + encoding. + :requestedScale: the scale needed to convert from the region width + and height to the output width and height. + """ + source = self.source + maxWidth = kwargs.get('output', {}).get('maxWidth') + maxHeight = kwargs.get('output', {}).get('maxHeight') + if ((maxWidth is not None and + (not isinstance(maxWidth, int) or maxWidth < 0)) or + (maxHeight is not None and + (not isinstance(maxHeight, int) or maxHeight < 0))): + msg = 'Invalid output width or height. Minimum value is 0.' + raise ValueError(msg) + + magLevel = None + mag = None + if maxWidth is None and maxHeight is None: + # If neither width nor height as specified, see if magnification, + # mm_x, or mm_y are requested. + magArgs = (kwargs.get('scale') or {}).copy() + magArgs['rounding'] = None + magLevel = source.getLevelForMagnification(**magArgs) + if magLevel is None and kwargs.get('scale', {}).get('exact'): + return None + mag = source.getMagnificationForLevel(magLevel) + metadata = source.metadata + left, top, right, bottom = source._getRegionBounds( + metadata, desiredMagnification=mag, **kwargs.get('region', {})) + regionWidth = right - left + regionHeight = bottom - top + magRequestedScale: Optional[float] = None + if maxWidth is None and maxHeight is None and mag: + if mag.get('scale') in (1.0, None): + maxWidth, maxHeight = regionWidth, regionHeight + magRequestedScale = 1 + else: + maxWidth = regionWidth / cast(float, mag['scale']) + maxHeight = regionHeight / cast(float, mag['scale']) + magRequestedScale = cast(float, mag['scale']) + outWidth, outHeight, calcScale = utilities._calculateWidthHeight( + maxWidth, maxHeight, regionWidth, regionHeight) + requestedScale = calcScale if magRequestedScale is None else magRequestedScale + if (regionWidth < 0 or regionHeight < 0 or outWidth == 0 or + outHeight == 0): + return None + + preferredLevel = metadata['levels'] - 1 + # If we are scaling the result, pick the tile level that is at least + # the resolution we need and is preferred by the tile source. + if outWidth != regionWidth or outHeight != regionHeight: + newLevel = source.getPreferredLevel(preferredLevel + int( + math.ceil(round(math.log(max(float(outWidth) / regionWidth, + float(outHeight) / regionHeight)) / + math.log(2), 4)))) + if newLevel < preferredLevel: + # scale the bounds to the level we will use + factor = 2 ** (preferredLevel - newLevel) + left = int(left / factor) + right = int(right / factor) + regionWidth = right - left + top = int(top / factor) + bottom = int(bottom / factor) + regionHeight = bottom - top + preferredLevel = newLevel + requestedScale /= factor + # If an exact magnification was requested and this tile source doesn't + # have tiles at the appropriate level, indicate that we won't return + # anything. + if (magLevel is not None and magLevel != preferredLevel and + kwargs.get('scale', {}).get('exact')): + return None + + tile_size = { + 'width': metadata['tileWidth'], + 'height': metadata['tileHeight'], + } + tile_overlap = { + 'x': int(kwargs.get('tile_overlap', {}).get('x', 0) or 0), + 'y': int(kwargs.get('tile_overlap', {}).get('y', 0) or 0), + 'edges': kwargs.get('tile_overlap', {}).get('edges', False), + 'offset_x': 0, + 'offset_y': 0, + 'range_x': 0, + 'range_y': 0, + } + if not tile_overlap['edges']: + # offset by half the overlap + tile_overlap['offset_x'] = tile_overlap['x'] // 2 + tile_overlap['offset_y'] = tile_overlap['y'] // 2 + tile_overlap['range_x'] = tile_overlap['x'] + tile_overlap['range_y'] = tile_overlap['y'] + if 'tile_size' in kwargs: + tile_size['width'] = int(kwargs['tile_size'].get( + 'width', kwargs['tile_size'].get('height', tile_size['width']))) + tile_size['height'] = int(kwargs['tile_size'].get( + 'height', kwargs['tile_size'].get('width', tile_size['height']))) + # Tile size includes the overlap + tile_size['width'] -= tile_overlap['x'] + tile_size['height'] -= tile_overlap['y'] + if tile_size['width'] <= 0 or tile_size['height'] <= 0: + msg = 'Invalid tile_size or tile_overlap.' + raise ValueError(msg) + + resample = ( + False if round(requestedScale, 2) == 1.0 or + kwargs.get('resample') in (None, False) else kwargs.get('resample')) + # If we need to resample to make tiles at a non-native resolution, + # adjust the tile size and tile overlap parameters appropriately. + if resample is not False: + tile_size['width'] = max(1, int(math.ceil(tile_size['width'] * requestedScale))) + tile_size['height'] = max(1, int(math.ceil(tile_size['height'] * requestedScale))) + tile_overlap['x'] = int(math.ceil(tile_overlap['x'] * requestedScale)) + tile_overlap['y'] = int(math.ceil(tile_overlap['y'] * requestedScale)) + + offset_x = kwargs.get('tile_offset', {}).get('left', 0) + offset_y = kwargs.get('tile_offset', {}).get('top', 0) + if kwargs.get('tile_offset', {}).get('auto'): + offset_x = left + offset_y = top + offset_x = (left - left % tile_size['width']) if offset_x > left else offset_x + offset_y = (top - top % tile_size['height']) if offset_y > top else offset_y + # If the overlapped tiles don't run over the edge, then the functional + # size of the region is reduced by the overlap. This factor is stored + # in the overlap offset_*. + xmin = int((left - offset_x) / tile_size['width']) + xmax = max(int(math.ceil((float(right - offset_x) - tile_overlap['range_x']) / + tile_size['width'])), xmin + 1) + ymin = int((top - offset_y) / tile_size['height']) + ymax = max(int(math.ceil((float(bottom - offset_y) - tile_overlap['range_y']) / + tile_size['height'])), ymin + 1) + tile_overlap.update({'xmin': xmin, 'xmax': xmax, + 'ymin': ymin, 'ymax': ymax}) + tile_overlap['offset_x'] += offset_x + tile_overlap['offset_y'] += offset_y + + # Use RGB for JPEG, RGBA for PNG + mode = 'RGBA' if kwargs.get('encoding') in {'PNG', 'TIFF', 'TILED'} else 'RGB' + + info = { + 'region': { + 'top': top, + 'left': left, + 'bottom': bottom, + 'right': right, + 'width': regionWidth, + 'height': regionHeight, + }, + 'xmin': xmin, + 'ymin': ymin, + 'xmax': xmax, + 'ymax': ymax, + 'mode': mode, + 'level': preferredLevel, + 'metadata': metadata, + 'output': { + 'width': outWidth, + 'height': outHeight, + }, + 'frame': kwargs.get('frame'), + 'format': kwargs.get('format', (TILE_FORMAT_NUMPY, )), + 'encoding': kwargs.get('encoding'), + 'requestedScale': requestedScale, + 'resample': resample, + 'tile_count': (xmax - xmin) * (ymax - ymin), + 'tile_overlap': tile_overlap, + 'tile_position': kwargs.get('tile_position'), + 'tile_size': tile_size, + } + return info + + def _tileIterator(self, iterInfo: Dict[str, Any]) -> Iterator[LazyTileDict]: + """ + Given tile iterator information, iterate through the tiles. + Each tile is returned as part of a dictionary that includes + + :x, y: (left, top) coordinate in current magnification pixels + :width, height: size of current tile in current magnification + pixels + :tile: cropped tile image + :format: format of the tile. One of TILE_FORMAT_NUMPY, + TILE_FORMAT_PIL, or TILE_FORMAT_IMAGE. TILE_FORMAT_IMAGE is + only returned if it was explicitly allowed and the tile is + already in the correct image encoding. + :level: level of the current tile + :level_x, level_y: the tile reference number within the level. + Tiles are numbered (0, 0), (1, 0), (2, 0), etc. The 0th tile + yielded may not be (0, 0) if a region is specified. + :tile_position: a dictionary of the tile position within the + iterator, containing: + + :level_x, level_y: the tile reference number within the level. + :region_x, region_y: 0, 0 is the first tile in the full + iteration (when not restricting the iteration to a single + tile). + :position: a 0-based value for the tile within the full + iteration. + + :iterator_range: a dictionary of the output range of the iterator: + + :level_x_min, level_x_max: the tiles that are be included + during the full iteration: [layer_x_min, layer_x_max). + :level_y_min, level_y_max: the tiles that are be included + during the full iteration: [layer_y_min, layer_y_max). + :region_x_max, region_y_max: the number of tiles included during + the full iteration. This is layer_x_max - layer_x_min, + layer_y_max - layer_y_min. + :position: the total number of tiles included in the full + iteration. This is region_x_max * region_y_max. + + :magnification: magnification of the current tile + :mm_x, mm_y: size of the current tile pixel in millimeters. + :gx, gy: (left, top) coordinates in maximum-resolution pixels + :gwidth, gheight: size of of the current tile in maximum-resolution + pixels. + :tile_overlap: the amount of overlap with neighboring tiles (left, + top, right, and bottom). Overlap never extends outside of the + requested region. + + If a region that includes partial tiles is requested, those tiles are + cropped appropriately. Most images will have tiles that get cropped + along the right and bottom edges in any case. + + :param iterInfo: tile iterator information. See _tileIteratorInfo. + :yields: an iterator that returns a dictionary as listed above. + """ + source = self.source + regionWidth = iterInfo['region']['width'] + regionHeight = iterInfo['region']['height'] + left = iterInfo['region']['left'] + top = iterInfo['region']['top'] + xmin = iterInfo['xmin'] + ymin = iterInfo['ymin'] + xmax = iterInfo['xmax'] + ymax = iterInfo['ymax'] + level = iterInfo['level'] + metadata = iterInfo['metadata'] + tileSize = iterInfo['tile_size'] + tileOverlap = iterInfo['tile_overlap'] + format = iterInfo['format'] + encoding = iterInfo['encoding'] + + source.logger.debug( + 'Fetching region of an image with a source size of %d x %d; ' + 'getting %d tile%s', + regionWidth, regionHeight, (xmax - xmin) * (ymax - ymin), + '' if (xmax - xmin) * (ymax - ymin) == 1 else 's') + + # If tile is specified, return at most one tile + if iterInfo.get('tile_position') is not None: + tilePos = iterInfo.get('tile_position') + if isinstance(tilePos, dict): + if tilePos.get('position') is not None: + tilePos = tilePos['position'] + elif 'region_x' in tilePos and 'region_y' in tilePos: + tilePos = (tilePos['region_x'] + + tilePos['region_y'] * (xmax - xmin)) + elif 'level_x' in tilePos and 'level_y' in tilePos: + tilePos = ((tilePos['level_x'] - xmin) + + (tilePos['level_y'] - ymin) * (xmax - xmin)) + if tilePos < 0 or tilePos >= (ymax - ymin) * (xmax - xmin): + xmax = xmin + else: + ymin += int(tilePos / (xmax - xmin)) + ymax = ymin + 1 + xmin += int(tilePos % (xmax - xmin)) + xmax = xmin + 1 + mag = source.getMagnificationForLevel(level) + scale = mag.get('scale', 1.0) + retile = (tileSize['width'] != metadata['tileWidth'] or + tileSize['height'] != metadata['tileHeight'] or + tileOverlap['x'] or tileOverlap['y']) + for y in range(ymin, ymax): + for x in range(xmin, xmax): + crop = None + posX = int(x * tileSize['width'] - tileOverlap['x'] // 2 + + tileOverlap['offset_x'] - left) + posY = int(y * tileSize['height'] - tileOverlap['y'] // 2 + + tileOverlap['offset_y'] - top) + tileWidth = tileSize['width'] + tileOverlap['x'] + tileHeight = tileSize['height'] + tileOverlap['y'] + # crop as needed + if (posX < 0 or posY < 0 or posX + tileWidth > regionWidth or + posY + tileHeight > regionHeight): + crop = (max(0, -posX), + max(0, -posY), + int(min(tileWidth, regionWidth - posX)), + int(min(tileHeight, regionHeight - posY))) + posX += crop[0] + posY += crop[1] + tileWidth = crop[2] - crop[0] + tileHeight = crop[3] - crop[1] + overlap = { + 'left': max(0, x * tileSize['width'] + tileOverlap['offset_x'] - left - posX), + 'top': max(0, y * tileSize['height'] + tileOverlap['offset_y'] - top - posY), + } + overlap['right'] = ( + max(0, tileWidth - tileSize['width'] - overlap['left']) + if x != xmin or not tileOverlap['range_x'] else + min(tileWidth, tileOverlap['range_x'] - tileOverlap['offset_x'])) + overlap['bottom'] = ( + max(0, tileHeight - tileSize['height'] - overlap['top']) + if y != ymin or not tileOverlap['range_y'] else + min(tileHeight, tileOverlap['range_y'] - tileOverlap['offset_y'])) + if tileOverlap['range_x']: + overlap['left'] = 0 if x == tileOverlap['xmin'] else overlap['left'] + overlap['right'] = 0 if x + 1 == tileOverlap['xmax'] else overlap['right'] + if tileOverlap['range_y']: + overlap['top'] = 0 if y == tileOverlap['ymin'] else overlap['top'] + overlap['bottom'] = 0 if y + 1 == tileOverlap['ymax'] else overlap['bottom'] + tile = LazyTileDict({ + 'x': x, + 'y': y, + 'frame': iterInfo.get('frame'), + 'level': level, + 'format': format, + 'encoding': encoding, + 'crop': crop, + 'requestedScale': iterInfo['requestedScale'], + 'retile': retile, + 'metadata': metadata, + 'source': source, + }, { + 'x': posX + left, + 'y': posY + top, + 'width': tileWidth, + 'height': tileHeight, + 'level': level, + 'level_x': x, + 'level_y': y, + 'magnification': mag['magnification'], + 'mm_x': mag['mm_x'], + 'mm_y': mag['mm_y'], + 'tile_position': { + 'level_x': x, + 'level_y': y, + 'region_x': x - iterInfo['xmin'], + 'region_y': y - iterInfo['ymin'], + 'position': ((x - iterInfo['xmin']) + + (y - iterInfo['ymin']) * + (iterInfo['xmax'] - iterInfo['xmin'])), + }, + 'iterator_range': { + 'level_x_min': iterInfo['xmin'], + 'level_y_min': iterInfo['ymin'], + 'level_x_max': iterInfo['xmax'], + 'level_y_max': iterInfo['ymax'], + 'region_x_max': iterInfo['xmax'] - iterInfo['xmin'], + 'region_y_max': iterInfo['ymax'] - iterInfo['ymin'], + 'position': ((iterInfo['xmax'] - iterInfo['xmin']) * + (iterInfo['ymax'] - iterInfo['ymin'])), + }, + 'tile_overlap': overlap, + }) + tile['gx'] = tile['x'] * scale + tile['gy'] = tile['y'] * scale + tile['gwidth'] = tile['width'] * scale + tile['gheight'] = tile['height'] * scale + yield tile
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image/tilesource/utilities.html b/_modules/large_image/tilesource/utilities.html new file mode 100644 index 000000000..0a9de86dd --- /dev/null +++ b/_modules/large_image/tilesource/utilities.html @@ -0,0 +1,1393 @@ + + + + + + large_image.tilesource.utilities — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image.tilesource.utilities

+import io
+import math
+import threading
+import types
+import xml.etree.ElementTree
+from collections import defaultdict
+from operator import attrgetter
+from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast
+
+import numpy as np
+import numpy.typing as npt
+import PIL
+import PIL.Image
+import PIL.ImageColor
+import PIL.ImageDraw
+
+from ..constants import dtypeToGValue
+
+# This was exposed here, once.
+
+try:
+    import simplejpeg
+except ImportError:
+    simplejpeg = None
+
+from ..constants import (TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY, TILE_FORMAT_PIL,
+                         TileOutputMimeTypes, TileOutputPILFormat)
+
+# Turn off decompression warning check
+PIL.Image.MAX_IMAGE_PIXELS = None
+
+# This is used by any submodule that uses vips to avoid a race condition in
+# new_from_file.  Since vips is technically optional and the various modules
+# might pull it in independently, it is located here to make is shareable.
+_newFromFileLock = threading.RLock()
+
+# Extend colors so G and GREEN map to expected values.  CSS green is #0080ff,
+# which is unfortunate.
+colormap = {
+    'R': '#ff0000',
+    'G': '#00ff00',
+    'B': '#0000ff',
+    'RED': '#ff0000',
+    'GREEN': '#00ff00',
+    'BLUE': '#0000ff',
+}
+modesBySize = ['L', 'LA', 'RGB', 'RGBA']
+
+
+
+[docs] +class ImageBytes(bytes): + """ + Wrapper class to make repr of image bytes better in ipython. + + Display the number of bytes and, if known, the mimetype. + """ + + def __new__(cls, source: bytes, mimetype: Optional[str] = None): + self = super().__new__(cls, source) + vars(self)['_mime_type'] = mimetype + return self + + @property + def mimetype(self) -> Optional[str]: + return vars(self)['_mime_type'] + + def _repr_png_(self) -> Optional[bytes]: + if self.mimetype == 'image/png': + return self + return None + + def _repr_jpeg_(self) -> Optional[bytes]: + if self.mimetype == 'image/jpeg': + return self + return None + + def __repr__(self) -> str: + if self.mimetype: + return f'ImageBytes<{len(self)}> ({self.mimetype})' + return f'ImageBytes<{len(self)}> (wrapped image bytes)'
+ + + +
+[docs] +class JSONDict(dict): + """Wrapper class to improve Jupyter repr of JSON-able dicts.""" + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + # TODO: validate JSON serializable? + + def _repr_json_(self) -> Dict: + return self
+ + + +def _encodeImageBinary( + image: PIL.Image.Image, encoding: str, jpegQuality: Union[str, int], + jpegSubsampling: Union[str, int], tiffCompression: str) -> bytes: + """ + Encode a PIL Image to a binary representation of the image (a jpeg, png, or + tif). + + :param image: a PIL image. + :param encoding: a valid PIL encoding (typically 'PNG' or 'JPEG'). Must + also be in the TileOutputMimeTypes map. + :param jpegQuality: the quality to use when encoding a JPEG. + :param jpegSubsampling: the subsampling level to use when encoding a JPEG. + :param tiffCompression: the compression format to use when encoding a TIFF. + :returns: a binary image or b'' if the image is of zero size. + """ + encoding = TileOutputPILFormat.get(encoding, encoding) + if image.width == 0 or image.height == 0: + return b'' + params: Dict[str, Any] = {} + if encoding == 'JPEG': + if image.mode not in ({'L', 'RGB', 'RGBA'} if simplejpeg else {'L', 'RGB'}): + image = image.convert('RGB' if image.mode != 'LA' else 'L') + if simplejpeg: + return ImageBytes(simplejpeg.encode_jpeg( + _imageToNumpy(image)[0], + quality=jpegQuality, + colorspace=image.mode if image.mode in {'RGB', 'RGBA'} else 'GRAY', + colorsubsampling={-1: '444', 0: '444', 1: '422', 2: '420'}.get( + cast(int, jpegSubsampling), str(jpegSubsampling).strip(':')), + ), mimetype='image/jpeg') + params['quality'] = jpegQuality + params['subsampling'] = jpegSubsampling + elif encoding in {'TIFF', 'TILED'}: + params['compression'] = { + 'none': 'raw', + 'lzw': 'tiff_lzw', + 'deflate': 'tiff_adobe_deflate', + }.get(tiffCompression, tiffCompression) + elif encoding == 'PNG': + params['compress_level'] = 2 + output = io.BytesIO() + try: + image.save(output, encoding, **params) + except Exception: + retry = True + if image.mode not in {'RGB', 'L'}: + image = image.convert('RGB') + try: + image.convert('RGB').save(output, encoding, **params) + retry = False + except Exception: + pass + if retry: + image.convert('1').save(output, encoding, **params) + return ImageBytes( + output.getvalue(), + mimetype=f'image/{encoding.lower().replace("tiled", "tiff")}', + ) + + +def _encodeImage( + image: Union[ImageBytes, PIL.Image.Image, bytes, np.ndarray], + encoding: str = 'JPEG', jpegQuality: int = 95, jpegSubsampling: int = 0, + format: Union[str, Tuple[str]] = (TILE_FORMAT_IMAGE, ), + tiffCompression: str = 'raw', **kwargs, +) -> Tuple[Union[ImageBytes, PIL.Image.Image, bytes, np.ndarray], str]: + """ + Convert a PIL or numpy image into raw output bytes, a numpy image, or a PIL + Image, and a mime type. + + :param image: a PIL image. + :param encoding: a valid PIL encoding (typically 'PNG' or 'JPEG'). Must + also be in the TileOutputMimeTypes map. + :param jpegQuality: the quality to use when encoding a JPEG. + :param jpegSubsampling: the subsampling level to use when encoding a JPEG. + :param format: the desired format or a tuple of allowed formats. Formats + are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, TILE_FORMAT_IMAGE). + :param tiffCompression: the compression format to use when encoding a TIFF. + :returns: + :imageData: the image data in the specified format and encoding. + :imageFormatOrMimeType: the image mime type if the format is + TILE_FORMAT_IMAGE, or the format of the image data if it is + anything else. + """ + if not isinstance(format, (tuple, set, list)): + format = (format, ) + imageData = image + imageFormatOrMimeType = TILE_FORMAT_PIL + if TILE_FORMAT_NUMPY in format: + imageData, _ = _imageToNumpy(image) + imageFormatOrMimeType = TILE_FORMAT_NUMPY + elif TILE_FORMAT_PIL in format: + imageData = _imageToPIL(image) + imageFormatOrMimeType = TILE_FORMAT_PIL + elif TILE_FORMAT_IMAGE in format: + if encoding not in TileOutputMimeTypes: + raise ValueError('Invalid encoding "%s"' % encoding) + imageFormatOrMimeType = TileOutputMimeTypes[encoding] + image = _imageToPIL(image) + imageData = _encodeImageBinary( + image, encoding, jpegQuality, jpegSubsampling, tiffCompression) + return imageData, imageFormatOrMimeType + + +def _imageToPIL( + image: Union[ImageBytes, PIL.Image.Image, bytes, np.ndarray], + setMode: Optional[str] = None) -> PIL.Image.Image: + """ + Convert an image in PIL, numpy, or image file format to a PIL image. + + :param image: input image. + :param setMode: if specified, the output image is converted to this mode. + :returns: a PIL image. + """ + if isinstance(image, np.ndarray): + mode = 'L' + if len(image.shape) == 3: + # Fallback for hyperspectral data to just use the first three bands + if image.shape[2] > 4: + image = image[:, :, :3] + mode = modesBySize[image.shape[2] - 1] + if len(image.shape) == 3 and image.shape[2] == 1: + image = np.resize(image, image.shape[:2]) + if image.dtype == np.uint32: + image = np.floor_divide(image, 2 ** 24).astype(np.uint8) + elif image.dtype == np.uint16: + image = np.floor_divide(image, 256).astype(np.uint8) + # TODO: The scaling of float data needs to be identical across all + # tiles of an image. This means that we need a reference to the parent + # tile source or some other way of regulating it. + # elif image.dtype.kind == 'f': + # if numpy.max(image) > 1: + # maxl2 = math.ceil(math.log(numpy.max(image) + 1) / math.log(2)) + # image = image / ((2 ** maxl2) - 1) + # image = (image * 255).astype(numpy.uint8) + elif image.dtype != np.uint8: + image = image.astype(np.uint8) + image = PIL.Image.fromarray(image, mode) + elif not isinstance(image, PIL.Image.Image): + image = PIL.Image.open(io.BytesIO(image)) + if setMode is not None and image.mode != setMode: + image = image.convert(setMode) + return image + + +def _imageToNumpy( + image: Union[ImageBytes, PIL.Image.Image, bytes, np.ndarray]) -> Tuple[np.ndarray, 'str']: + """ + Convert an image in PIL, numpy, or image file format to a numpy array. The + output numpy array always has three dimensions. + + :param image: input image. + :returns: a numpy array and a target PIL image mode. + """ + if isinstance(image, np.ndarray) and len(image.shape) == 3 and 1 <= image.shape[2] <= 4: + return image, modesBySize[image.shape[2] - 1] + if (simplejpeg and isinstance(image, bytes) and image[:3] == b'\xff\xd8\xff' and + b'\xff\xc0' in image[:1024]): + idx = image.index(b'\xff\xc0') + if image[idx + 9:idx + 10] in {b'\x01', b'\x03'}: + try: + image = simplejpeg.decode_jpeg( + image, colorspace='GRAY' if image[idx + 9:idx + 10] == b'\x01' else 'RGB') + except Exception: + pass + if not isinstance(image, np.ndarray): + if not isinstance(image, PIL.Image.Image): + image = PIL.Image.open(io.BytesIO(image)) + if image.mode not in ('L', 'LA', 'RGB', 'RGBA'): + image = image.convert('RGBA') + mode = image.mode + if not image.width or not image.height: + image = np.zeros((image.height, image.width, len(mode))) + else: + image = np.asarray(image) + else: + if len(image.shape) == 3: + mode = modesBySize[(image.shape[2] - 1) if image.shape[2] <= 4 else 3] + return image, mode + else: + mode = 'L' + if len(image.shape) == 2: + image = np.resize(image, (image.shape[0], image.shape[1], 1)) + return image, mode + + +def _letterboxImage(image: PIL.Image.Image, width: int, height: int, fill: str) -> PIL.Image.Image: + """ + Given a PIL image, width, height, and fill color, letterbox or pillarbox + the image to make it the specified dimensions. The image is never + cropped. The original image will be returned if no action is needed. + + :param image: the source image. + :param width: the desired width in pixels. + :param height: the desired height in pixels. + :param fill: a fill color. + """ + if ((image.width >= width and image.height >= height) or + not fill or str(fill).lower() == 'none'): + return image + corner = False + if fill.lower().startswith('corner:'): + corner, fill = True, fill.split(':', 1)[1] + color = PIL.ImageColor.getcolor(colormap.get(fill, fill), image.mode) + width = max(width, image.width) + height = max(height, image.height) + result = PIL.Image.new(image.mode, (width, height), color) + result.paste(image, ( + int((width - image.width) / 2) if not corner else 0, + int((height - image.height) / 2) if not corner else 0)) + return result + + +def _vipsCast(image: Any, mustBe8Bit: bool = False) -> Any: + """ + Cast a vips image to a format we want. + + :param image: a vips image + :param mustBe9Bit: if True, then always cast to unsigned 8-bit. + :returns: a vips image + """ + import pyvips + + image = cast(pyvips.Image, image) + formats = { + pyvips.BandFormat.CHAR: (pyvips.BandFormat.UCHAR, 2**7, 1), + pyvips.BandFormat.COMPLEX: (pyvips.BandFormat.USHORT, 0, 65535), + pyvips.BandFormat.DOUBLE: (pyvips.BandFormat.USHORT, 0, 65535), + pyvips.BandFormat.DPCOMPLEX: (pyvips.BandFormat.USHORT, 0, 65535), + pyvips.BandFormat.FLOAT: (pyvips.BandFormat.USHORT, 0, 65535), + pyvips.BandFormat.INT: (pyvips.BandFormat.USHORT, 2**31, 2**-16), + pyvips.BandFormat.USHORT: (pyvips.BandFormat.UCHAR, 0, 2**-8), + pyvips.BandFormat.SHORT: (pyvips.BandFormat.USHORT, 2**15, 1), + pyvips.BandFormat.UINT: (pyvips.BandFormat.USHORT, 0, 2**-16), + } + if image.format not in formats or (image.format == pyvips.BandFormat.USHORT and not mustBe8Bit): + return image + target, offset, multiplier = formats[image.format] + if image.format == pyvips.BandFormat.DOUBLE or image.format == pyvips.BandFormat.FLOAT: + maxVal = image.max() + # These thresholds are higher than 256 and 65536 because bicubic and + # other interpolations can cause value spikes + if maxVal >= 2 and maxVal < 2**9: + multiplier = 256 + elif maxVal >= 256 and maxVal < 2**17: + multiplier = 1 + if mustBe8Bit and target != pyvips.BandFormat.UCHAR: + target = pyvips.BandFormat.UCHAR + multiplier /= 256 + # logger.debug('Casting image from %r to %r', image.format, target) + image = ((image.cast(pyvips.BandFormat.DOUBLE) + offset) * multiplier).cast(target) + return image + + +def _rasterioParameters( + defaultCompression: Optional[str] = None, + eightbit: Optional[bool] = None, **kwargs) -> Dict[str, Any]: + """ + Return a dictionary of creation option for the rasterio driver + + :param defaultCompression: if not specified, use this value. + :param eightbit: True or False to indicate that the bit depth per sample + is known. None for unknown. + + Optional parameters that can be specified in kwargs: + + :param tileSize: the horizontal and vertical tile size. + :param compression: one of 'jpeg', 'deflate' (zip), 'lzw', 'packbits', + zstd', or 'none'. + :param quality: a jpeg quality passed to gdal. 0 is small, 100 is high + uality. 90 or above is recommended. + :param level: compression level for zstd, 1-22 (default is 10). + :param predictor: one of 'none', 'horizontal', or 'float' used for lzw and deflate. + + :returns: a dictionary of parameters. + """ + # some default option and parameters + options = {'blocksize': 256, 'compress': 'lzw', 'quality': 90} + + # the name of the predictor need to be strings so we convert here from set values to actual + # required values (https://rasterio.readthedocs.io/en/latest/topics/image_options.html) + predictor = {'none': 'NO', 'horizontal': 'STANDARD', 'float': 'FLOATING_POINT', 'yes': 'YES'} + + if eightbit is not None: + options['predictor'] = 'yes' if eightbit else 'none' + + # add the values from kwargs to the options. Remove anything that isnot set. + options.update({k: v for k, v in kwargs.items() if v not in (None, '')}) + + # add the remaining options + options.update(tiled=True, bigtiff='IF_SAFER') + 'predictor' not in options or options.update(predictor=predictor[str(options['predictor'])]) + + return options + + +def _gdalParameters( + defaultCompression: Optional[str] = None, + eightbit: Optional[bool] = None, **kwargs) -> List[str]: + """ + Return an array of gdal translation parameters. + + :param defaultCompression: if not specified, use this value. + :param eightbit: True or False to indicate that the bit depth per sample is + known. None for unknown. + + Optional parameters that can be specified in kwargs: + + :param tileSize: the horizontal and vertical tile size. + :param compression: one of 'jpeg', 'deflate' (zip), 'lzw', 'packbits', + 'zstd', or 'none'. + :param quality: a jpeg quality passed to gdal. 0 is small, 100 is high + quality. 90 or above is recommended. + :param level: compression level for zstd, 1-22 (default is 10). + :param predictor: one of 'none', 'horizontal', or 'float' used for lzw and + deflate. + :returns: a dictionary of parameters. + """ + options = _rasterioParameters( + defaultCompression=defaultCompression, + eightbit=eightbit, + **kwargs) + # Remap for different names bewtwee rasterio/gdal + options['tileSize'] = options.pop('blocksize') + options['compression'] = options.pop('compress') + cmdopt = ['-of', 'COG', '-co', 'BIGTIFF=%s' % options['bigtiff']] + cmdopt += ['-co', 'BLOCKSIZE=%d' % options['tileSize']] + cmdopt += ['-co', 'COMPRESS=%s' % options['compression'].upper()] + cmdopt += ['-co', 'QUALITY=%s' % options['quality']] + if 'predictor' in options: + cmdopt += ['-co', 'PREDICTOR=%s' % options['predictor']] + if 'level' in options: + cmdopt += ['-co', 'LEVEL=%s' % options['level']] + return cmdopt + + +def _vipsParameters( + forTiled: bool = True, defaultCompression: Optional[str] = None, + **kwargs) -> Dict[str, Any]: + """ + Return a dictionary of vips conversion parameters. + + :param forTiled: True if this is for a tiled image. False for an + associated image. + :param defaultCompression: if not specified, use this value. + + Optional parameters that can be specified in kwargs: + + :param tileSize: the horizontal and vertical tile size. + :param compression: one of 'jpeg', 'deflate' (zip), 'lzw', 'packbits', + 'zstd', or 'none'. + :param quality: a jpeg quality passed to vips. 0 is small, 100 is high + quality. 90 or above is recommended. + :param level: compression level for zstd, 1-22 (default is 10). + :param predictor: one of 'none', 'horizontal', or 'float' used for lzw and + deflate. + :param shrinkMode: one of vips's VipsRegionShrink strings. + :returns: a dictionary of parameters. + """ + if not forTiled: + convertParams = { + 'compression': defaultCompression or 'jpeg', + 'Q': 90, + 'predictor': 'horizontal', + 'tile': False, + } + if 'mime' in kwargs and kwargs.get('mime') != 'image/jpeg': + convertParams['compression'] = 'lzw' + return convertParams + convertParams = { + 'tile': True, + 'tile_width': 256, + 'tile_height': 256, + 'pyramid': True, + 'bigtiff': True, + 'compression': defaultCompression or 'jpeg', + 'Q': 90, + 'predictor': 'horizontal', + } + # For lossless modes, make sure pixel values in lower resolutions are + # values that exist in the upper resolutions. + if convertParams['compression'] in {'none', 'lzw'}: + convertParams['region_shrink'] = 'nearest' + if kwargs.get('shrinkMode') and kwargs['shrinkMode'] != 'default': + convertParams['region_shrink'] = kwargs['shrinkMode'] + for vkey, kwkeys in { + 'tile_width': {'tileSize'}, + 'tile_height': {'tileSize'}, + 'compression': {'compression', 'tiffCompression'}, + 'Q': {'quality', 'jpegQuality'}, + 'level': {'level'}, + 'predictor': {'predictor'}, + }.items(): + for kwkey in kwkeys: + if kwkey in kwargs and kwargs[kwkey] not in {None, ''}: + convertParams[vkey] = kwargs[kwkey] + if convertParams['compression'] == 'jp2k': + convertParams['compression'] = 'none' + if convertParams['compression'] == 'webp' and kwargs.get('quality') == 0: + convertParams['lossless'] = True + convertParams.pop('Q', None) + if convertParams['predictor'] == 'yes': + convertParams['predictor'] = 'horizontal' + if convertParams['compression'] == 'jpeg': + convertParams['rgbjpeg'] = True + return convertParams + + +
+[docs] +def etreeToDict(t: xml.etree.ElementTree.Element) -> Dict[str, Any]: + """ + Convert an xml etree to a nested dictionary without schema names in the + keys. If you have an xml string, this can be converted to a dictionary via + xml.etree.etreeToDict(ElementTree.fromstring(xml_string)). + + :param t: an etree. + :returns: a python dictionary with the results. + """ + # Remove schema + tag = t.tag.split('}', 1)[1] if t.tag.startswith('{') else t.tag + d: Dict[str, Any] = {tag: {}} + children = list(t) + if children: + entries = defaultdict(list) + for entry in map(etreeToDict, children): + for k, v in entry.items(): + entries[k].append(v) + d = {tag: {k: v[0] if len(v) == 1 else v + for k, v in entries.items()}} + + if t.attrib: + d[tag].update({(k.split('}', 1)[1] if k.startswith('{') else k): v + for k, v in t.attrib.items()}) + text = (t.text or '').strip() + if text and len(d[tag]): + d[tag]['text'] = text + elif text: + d[tag] = text + return d
+ + + +
+[docs] +def dictToEtree( + d: Dict[str, Any], + root: Optional[xml.etree.ElementTree.Element] = None) -> xml.etree.ElementTree.Element: + """ + Convert a dictionary in the style produced by etreeToDict back to an etree. + Make an xml string via xml.etree.ElementTree.tostring(dictToEtree( + dictionary), encoding='utf8', method='xml'). Note that this function and + etreeToDict are not perfect conversions; numerical values are quoted in + xml. Plain key-value pairs are ambiguous whether they should be attributes + or text values. Text fields are collected together. + + :param d: a dictionary. + :prarm root: the root node to attach this dictionary to. + :returns: an etree. + """ + if root is None: + if len(d) == 1: + k, v = next(iter(d.items())) + root = xml.etree.ElementTree.Element(k) + dictToEtree(v, root) + return root + root = xml.etree.ElementTree.Element('root') + for k, v in d.items(): + if isinstance(v, list): + for l in v: + elem = xml.etree.ElementTree.SubElement(root, k) + dictToEtree(l, elem) + elif isinstance(v, dict): + elem = xml.etree.ElementTree.SubElement(root, k) + dictToEtree(v, elem) + else: + if k == 'text': + root.text = v + else: + root.set(k, v) + return root
+ + + +
+[docs] +def nearPowerOfTwo(val1: float, val2: float, tolerance: float = 0.02) -> bool: + """ + Check if two values are different by nearly a power of two. + + :param val1: the first value to check. + :param val2: the second value to check. + :param tolerance: the maximum difference in the log2 ratio's mantissa. + :return: True if the values are nearly a power of two different from each + other; false otherwise. + """ + # If one or more of the values is zero or they have different signs, then + # return False + if val1 * val2 <= 0: + return False + log2ratio = math.log(float(val1) / float(val2)) / math.log(2) + # Compare the mantissa of the ratio's log2 value. + return abs(log2ratio - round(log2ratio)) < tolerance
+ + + +def _arrayToPalette(palette: List[Union[str, float, Tuple[float, ...]]]) -> np.ndarray: + """ + Given an array of color strings, tuples, or lists, return a numpy array. + + :param palette: an array of color strings, tuples, or lists. + :returns: a numpy array of RGBA value on the scale of [0-255]. + """ + arr: List[Union[np.ndarray, Tuple[float, ...]]] = [] + for clr in palette: + if isinstance(clr, (tuple, list)): + arr.append(np.array((list(clr) + [1, 1, 1])[:4]) * 255) + else: + try: + arr.append(PIL.ImageColor.getcolor(str(colormap.get(str(clr), clr)), 'RGBA')) + except ValueError: + try: + import matplotlib as mpl + + arr.append(PIL.ImageColor.getcolor(mpl.colors.to_hex(cast(str, clr)), 'RGBA')) + except (ImportError, ValueError): + raise ValueError('cannot be used as a color palette: %r.' % palette) + return np.array(arr) + + +
+[docs] +def getPaletteColors(value: Union[str, List[Union[str, float, Tuple[float, ...]]]]) -> np.ndarray: + """ + Given a list or a name, return a list of colors in the form of a numpy + array of RGBA. If a list, each entry is a color name resolvable by either + PIL.ImageColor.getcolor, by matplotlib.colors, or a 3 or 4 element list or + tuple of RGB(A) values on a scale of 0-1. If this is NOT a list, then, if + it can be parsed as a color, it is treated as ['#000', <value>]. If that + cannot be parsed, then it is assumed to be a named palette in palettable + (such as viridis.Viridis_12) or a named palette in matplotlib (including + plugins). + + :param value: Either a list, a single color name, or a palette name. See + above. + :returns: a numpy array of RGBA value on the scale of [0-255]. + """ + palette = None + if isinstance(value, (tuple, list)): + palette = value + if palette is None: + try: + PIL.ImageColor.getcolor(str(colormap.get(str(value), value)), 'RGBA') + palette = ['#000', str(value)] + except ValueError: + pass + if palette is None: + import palettable + + try: + palette = attrgetter(str(value))(palettable).hex_colors + except AttributeError: + pass + if palette is None: + try: + import matplotlib as mpl + + if value in mpl.colors.get_named_colors_mapping(): + palette = ['#0000', mpl.colors.to_hex(str(value))] + else: + cmap = (mpl.colormaps.get_cmap(str(value)) if hasattr(getattr( + mpl, 'colormaps', None), 'get_cmap') else + mpl.cm.get_cmap(str(value))) + palette = [mpl.colors.to_hex(cmap(i)) for i in range(cmap.N)] + except (ImportError, ValueError, AttributeError): + pass + if palette is None: + raise ValueError('cannot be used as a color palette.: %r.' % value) + return _arrayToPalette(palette)
+ + + +
+[docs] +def isValidPalette(value: Union[str, List[Union[str, float, Tuple[float, ...]]]]) -> bool: + """ + Check if a value can be used as a palette. + + :param value: Either a list, a single color name, or a palette name. See + getPaletteColors. + :returns: a boolean; true if the value can be used as a palette. + """ + try: + getPaletteColors(value) + return True + except ValueError: + return False
+ + + +def _recursePalettablePalettes( + module: types.ModuleType, palettes: Set[str], + root: Optional[str] = None, depth: int = 0) -> None: + """ + Walk the modules in palettable to find all of the available palettes. + + :param module: the current module. + :param palettes: a set to add palette names to. + :param root: a string of the parent modules. None for palettable itself. + :param depth: the depth of the walk. Used to avoid needless recursion. + """ + for key in dir(module): + if not key.startswith('_'): + attr = getattr(module, key) + if isinstance(attr, types.ModuleType) and depth < 3: + _recursePalettablePalettes( + attr, palettes, root + '.' + key if root else key, depth + 1) + elif root and isinstance(getattr(attr, 'hex_colors', None), list): + palettes.add(root + '.' + key) + + +
+[docs] +def getAvailableNamedPalettes(includeColors: bool = True, reduced: bool = False) -> List[str]: + """ + Get a list of all named palettes that can be used with getPaletteColors. + + :param includeColors: if True, include named colors. If False, only + include actual palettes. + :param reduced: if True, exclude reversed palettes and palettes with + fewer colors where a palette with the same basic name exists with more + colors. + :returns: a list of names. + """ + import palettable + + palettes = set() + if includeColors: + palettes |= set(PIL.ImageColor.colormap.keys()) + palettes |= set(colormap.keys()) + _recursePalettablePalettes(palettable, palettes) + try: + import matplotlib as mpl + + if includeColors: + palettes |= set(mpl.colors.get_named_colors_mapping()) + # matplotlib has made the colormap list more public in recent versions + mplcm = (mpl.colormaps if hasattr(mpl, 'colormaps') + else mpl.cm._cmap_registry) # type: ignore + for key in mplcm: + if isValidPalette(key): + palettes.add(key) + except ImportError: + pass + if reduced: + palettes = { + key for key in palettes + if not key.endswith('_r') and ( + '_' not in key or + not key.rsplit('_', 1)[-1].isdigit() or + (key.rsplit('_', 1)[0] + '_' + str(int( + key.rsplit('_', 1)[-1]) + 1)) not in palettes)} + return sorted(palettes)
+ + + +
+[docs] +def fullAlphaValue(arr: Union[np.ndarray, npt.DTypeLike]) -> int: + """ + Given a numpy array, return the value that should be used for a fully + opaque alpha channel. For uint variants, this is the max value. + + :param arr: a numpy array. + :returns: the value for the alpha channel. + """ + dtype = arr.dtype if isinstance(arr, np.ndarray) else arr + if dtype.kind == 'u': + return np.iinfo(dtype).max + return 1
+ + + +def _makeSameChannelDepth(arr1: np.ndarray, arr2: np.ndarray) -> Tuple[np.ndarray, np.ndarray]: + """ + Given two numpy arrays that are either two or three dimensions, make the + third dimension the same for both of them. Specifically, if there are two + dimensions, first convert to three dimensions with a single final value. + Otherwise, the dimensions are assumed to be channels of L, LA, RGB, RGBA, + or <all colors>. If L is needed to change to RGB, it is repeated (LLL). + Missing A channels are filled with 255, 65535, or 1 depending on if the + dtype is uint8, uint16, or something else. + + :param arr1: one array to compare. + :param arr2: a second array to compare. + :returns: the two arrays, possibly modified. + """ + arrays = { + 'arr1': arr1, + 'arr2': arr2, + } + # Make sure we have 3 dimensional arrays + for key, arr in arrays.items(): + if len(arr.shape) == 2: + arrays[key] = np.resize(arr, (arr.shape[0], arr.shape[1], 1)) + # If any array is RGB, make sure all arrays are RGB. + for key, arr in arrays.items(): + other = arrays['arr1' if key == 'arr2' else 'arr2'] + if arr.shape[2] < 3 and other.shape[2] >= 3: # type: ignore[misc] + newarr = np.ones( + (arr.shape[0], arr.shape[1], arr.shape[2] + 2), # type: ignore[misc] + dtype=arr.dtype) + newarr[:, :, 0:1] = arr[:, :, 0:1] + newarr[:, :, 1:2] = arr[:, :, 0:1] + newarr[:, :, 2:3] = arr[:, :, 0:1] + if arr.shape[2] == 2: # type: ignore[misc] + newarr[:, :, 3:4] = arr[:, :, 1:2] + arrays[key] = newarr + # If only one array has an A channel, make sure all arrays have an A + # channel + for key, arr in arrays.items(): + other = arrays['arr1' if key == 'arr2' else 'arr2'] + if arr.shape[2] < other.shape[2]: # type: ignore[misc] + arrays[key] = np.pad( + arr, + ((0, 0), (0, 0), (0, other.shape[2] - arr.shape[2])), # type: ignore[misc] + constant_values=fullAlphaValue(arr)) + return arrays['arr1'], arrays['arr2'] + + +def _addSubimageToImage( + image: Optional[np.ndarray], subimage: np.ndarray, x: int, y: int, + width: int, height: int) -> np.ndarray: + """ + Add a subimage to a larger image as numpy arrays. + + :param image: the output image record. None for not created yet. + :param subimage: a numpy array with the sub-image to add. + :param x: the location of the upper left point of the sub-image within + the output image. + :param y: the location of the upper left point of the sub-image within + the output image. + :param width: the output image size. + :param height: the output image size. + :returns: the output image record. + """ + if image is None: + if (x, y, width, height) == (0, 0, subimage.shape[1], subimage.shape[0]): + return subimage + image = np.empty( + (height, width, subimage.shape[2]), # type: ignore[misc] + dtype=subimage.dtype) + elif len(image.shape) != len(subimage.shape) or image.shape[-1] != subimage.shape[-1]: + image, subimage = _makeSameChannelDepth(image, subimage) + if subimage.shape[-1] in {2, 4}: + mask = (subimage[:, :, -1] > 0)[:, :, np.newaxis] + image[y:y + subimage.shape[0], x:x + subimage.shape[1]] = np.where( + mask, subimage, image[y:y + subimage.shape[0], x:x + subimage.shape[1]]) + else: + image[y:y + subimage.shape[0], x:x + subimage.shape[1]] = subimage + return image + + +def _vipsAddAlphaBand(vimg: Any, otherImages: List[Any]) -> Any: + """ + Add an alpha band to a vips image. The alpha value is either 1, 255, or + 65535 depending on the max value in the image and any other images passed + for reference. + + :param vimg: the vips image to modify. + :param otherImages: a list of other vips images to use for determining the + alpha value. + :returns: the original image with an alpha band. + """ + maxValue = vimg.max() + for img in otherImages: + maxValue = max(maxValue, img.max()) + alpha = 1 + if maxValue >= 2 and maxValue < 2**9: + alpha = 255 + elif maxValue >= 2**8 and maxValue < 2**17: + alpha = 65535 + return vimg.bandjoin(alpha) + + +def _addRegionTileToTiled( + image: Optional[Dict[str, Any]], subimage: np.ndarray, x: int, y: int, + width: int, height: int, tile: Dict[str, Any], **kwargs) -> Dict[str, Any]: + """ + Add a subtile to a vips image. + + :param image: an object with information on the output. + :param subimage: a numpy array with the sub-image to add. + :param x: the location of the upper left point of the sub-image within the + output image. + :param y: the location of the upper left point of the sub-image within the + output image. + :param width: the output image size. + :param height: the output image size. + :param tile: the original tile record with the current scale, etc. + :returns: the output object. + """ + import pyvips + + if subimage.dtype.char not in dtypeToGValue: + subimage = subimage.astype('d') + vimgMem = pyvips.Image.new_from_memory( + np.ascontiguousarray(subimage).data, + subimage.shape[1], subimage.shape[0], subimage.shape[2], # type: ignore[misc] + dtypeToGValue[subimage.dtype.char]) + vimg = pyvips.Image.new_temp_file('%s.v') + vimgMem.write(vimg) + if image is None: + image = { + 'width': width, + 'height': height, + 'mm_x': tile.get('mm_x') if tile else None, + 'mm_y': tile.get('mm_y') if tile else None, + 'magnification': tile.get('magnification') if tile else None, + 'channels': subimage.shape[2], # type: ignore[misc] + 'strips': {}, + } + if y not in image['strips']: + image['strips'][y] = vimg + if not x: + return image + if image['strips'][y].bands + 1 == vimg.bands: + image['strips'][y] = _vipsAddAlphaBand(image['strips'][y], vimg) + elif vimg.bands + 1 == image['strips'][y].bands: + vimg = _vipsAddAlphaBand(vimg, image['strips'][y]) + image['strips'][y] = image['strips'][y].insert(vimg, x, 0, expand=True) + return image + + +def _calculateWidthHeight( + width: Optional[float], height: Optional[float], regionWidth: float, + regionHeight: float) -> Tuple[int, int, float]: + """ + Given a source width and height and a maximum destination width and/or + height, calculate a destination width and height that preserves the aspect + ratio of the source. + + :param width: the destination width. None to only use height. + :param height: the destination height. None to only use width. + :param regionWidth: the width of the source data. + :param regionHeight: the height of the source data. + :returns: the width and height that is no larger than that specified and + preserves aspect ratio, and the scaling factor used for the conversion. + """ + if regionWidth == 0 or regionHeight == 0: + return 0, 0, 1 + # Constrain the maximum size if both width and height weren't + # specified, in case the image is very short or very narrow. + if height and not width: + width = height * 16 + if width and not height: + height = width * 16 + scaledWidth = max(1, int(regionWidth * cast(float, height) / regionHeight)) + scaledHeight = max(1, int(regionHeight * cast(float, width) / regionWidth)) + if scaledWidth == width or ( + cast(float, width) * regionHeight > cast(float, height) * regionWidth and + not scaledHeight == height): + scale = float(regionHeight) / cast(float, height) + width = scaledWidth + else: + scale = float(regionWidth) / cast(float, width) + height = scaledHeight + return int(cast(float, width)), int(cast(float, height)), scale + + +def _computeFramesPerTexture( + opts: Dict[str, Any], numFrames: int, sizeX: int, + sizeY: int) -> Tuple[int, int, int, int, int]: + """ + Compute the number of frames for each tile_frames texture. + + :param opts: the options dictionary from getTileFramesQuadInfo. + :param numFrames: the number of frames that need to be included. + :param sizeX: the size of one frame of the image. + :param sizeY: the size of one frame of the image. + :returns: + :fw: the width of an individual frame in the texture. + :fh: the height of an individual frame in the texture. + :fhorz: the number of frames across the texture. + :fperframe: the number of frames per texture. The last texture may + have fewer frames. + :textures: the number of textures to be used. This many calls will + need to be made to tileFrames. + """ + # defining fw, fh, fhorz, fvert, fperframe + alignment = opts['alignment'] or 16 + texSize = opts['maxTextureSize'] + textures = opts['maxTextures'] or 1 + while texSize ** 2 > opts['maxTotalTexturePixels']: + texSize //= 2 + while textures > 1 and texSize ** 2 * textures > opts['maxTotalTexturePixels']: + textures -= 1 + # Iterate in case we can reduce the number of textures or the texture size + while True: + f = int(math.ceil(numFrames / textures)) # frames per texture + if opts['frameGroup'] > 1: + fg = int(math.ceil(f / opts['frameGroup'])) * opts['frameGroup'] + if fg / f <= opts['frameGroupFactor']: + f = fg + texScale2 = texSize ** 2 / f / sizeX / sizeY + # frames across the texture + fhorz = int(math.ceil(texSize / (math.ceil( + sizeX * texScale2 ** 0.5 / alignment) * alignment))) + fvert = int(math.ceil(texSize / (math.ceil( + sizeY * texScale2 ** 0.5 / alignment) * alignment))) + # tile sizes + fw = int(math.floor(texSize / fhorz / alignment)) * alignment + fvert = int(max(math.ceil(f / (texSize // fw)), fvert)) + fh = int(math.floor(texSize / fvert / alignment) * alignment) + if opts['maxFrameSize']: + maxFrameSize = opts['maxFrameSize'] // alignment * alignment + fw = min(fw, maxFrameSize) + fh = min(fh, maxFrameSize) + if fw > sizeX: + fw = int(math.ceil(sizeX / alignment) * alignment) + if fh > sizeY: + fh = int(math.ceil(sizeY / alignment) * alignment) + # shrink one dimension to account for aspect ratio + fw = int(min(math.ceil(fh * sizeX / sizeY / alignment) * alignment, fw)) + fh = int(min(math.ceil(fw * sizeY / sizeX / alignment) * alignment, fh)) + # recompute frames across the texture + fhorz = texSize // fw + fvert = int(min(texSize // fh, math.ceil(numFrames / fhorz))) + fperframe = fhorz * fvert + if textures > 1 and opts['frameGroup'] > 1: + fperframe = int(fperframe // opts['frameGroup'] * opts['frameGroup']) + if textures * fperframe < numFrames and fhorz * fvert * textures >= numFrames: + fperframe = fhorz * fvert + # check if we are not using all textures or are using less than a + # quarter of one texture. If not, stop, if so, reduce and recalculate + if textures > 1 and numFrames <= fperframe * (textures - 1): + textures -= 1 + continue + if fhorz >= 2 and math.ceil(f / (fhorz // 2)) * fh <= texSize / 2: + texSize //= 2 + continue + return fw, fh, fhorz, fperframe, textures + + +
+[docs] +def getTileFramesQuadInfo( + metadata: Dict[str, Any], options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """ + Compute what tile_frames need to be requested for a particular condition. + + Options is a dictionary of: + :format: The compression and format for the texture. Defaults to + {'encoding': 'JPEG', 'jpegQuality': 85, 'jpegSubsampling': 1}. + :query: Additional query options to add to the tile source, such as + style. + :frameBase: (default 0) Starting frame number used. c/z/xy/z to step + through that index length (0 to 1 less than the value), which is + probably only useful for cache reporting or scheduling. + :frameStride: (default 1) Only use every ``frameStride`` frame of the + image. c/z/xy/z to use that axis length. + :frameGroup: (default 1) If above 1 and multiple textures are used, each + texture will have an even multiple of the group size number of + frames. This helps control where texture loading transitions + occur. c/z/xy/z to use that axis length. + :frameGroupFactor: (default 4) If ``frameGroup`` would reduce the size + of the tile images beyond this factor, don't use it. + :frameGroupStride: (default 1) If ``frameGroup`` is above 1 and multiple + textures are used, then the frames are reordered based on this + stride value. "auto" to use frameGroup / frameStride if that + value is an integer. + :maxTextureSize: Limit the maximum texture size to a square of this + size. + :maxTextures: (default 1) If more than one, allow multiple textures to + increase the size of the individual frames. The number of textures + will be capped by ``maxTotalTexturePixels`` as well as this number. + :maxTotalTexturePixels: (default 1073741824) Limit the maximum texture + size and maximum number of textures so that the combined set does + not exceed this number of pixels. + :alignment: (default 16) Individual frames are buffered to an alignment + of this maxy pixels. If JPEG compression is used, this should + be 8 for monochrome images or jpegs without subsampling, or 16 for + jpegs with moderate subsampling to avoid compression artifacts from + leaking between frames. + :maxFrameSize: If set, limit the maximum width and height of an + individual frame to this value. + + + :param metadata: the tile source metadata. Needs to contain sizeX, sizeY, + tileWidth, tileHeight, and a list of frames. + :param options: dictionary of options, as described above. + :returns: a dictionary of values to use for making calls to tile_frames. + """ + defaultOptions = { + 'format': { + 'encoding': 'JPEG', + 'jpegQuality': 85, + 'jpegSubsampling': 1, + }, + 'query': {}, + 'frameBase': 0, + 'frameStride': 1, + 'frameGroup': 1, + 'frameGroupFactor': 4, + 'frameGroupStride': 1, + 'maxTextureSize': 8192, + 'maxTextures': 1, + 'maxTotalTexturePixels': 1024 * 1024 * 1024, + 'alignment': 16, + 'maxFrameSize': None, + } + opts = defaultOptions.copy() + opts.update(options or {}) + + opts['frameStride'] = ( + int(cast(Union[str, int], opts['frameStride'])) if str(opts['frameStride']).isdigit() else + metadata.get('IndexRange', {}).get('Index' + str(opts['frameStride']).upper(), 1)) + opts['frameGroup'] = ( + int(cast(Union[str, int], opts['frameGroup'])) if str(opts['frameGroup']).isdigit() else + metadata.get('IndexRange', {}).get('Index' + str(opts['frameGroup']).upper(), 1)) + opts['frameGroupStride'] = ( + int(cast(Union[str, int], opts['frameGroupStride'])) + if opts['frameGroupStride'] != 'auto' else + max(1, cast(int, opts['frameGroup']) // cast(int, opts['frameStride']))) + if str(opts['frameBase']).isdigit(): + opts['frameBase'] = int(cast(Union[str, int], opts['frameBase'])) + else: + statusidx = { + 'metadata': metadata, + 'options': opts, + 'src': [], + } + for val in range(metadata.get( + 'IndexRange', {}).get('Index' + str(opts['frameBase']).upper(), 1)): + opts['frameBase'] = val + result = getTileFramesQuadInfo(metadata, opts) + cast(List[Any], statusidx['src']).extend(cast(List[Any], result['src'])) + return statusidx + sizeX, sizeY = metadata['sizeX'], metadata['sizeY'] + numFrames = len(metadata.get('frames', [])) or 1 + frames = [] + for fds in range(cast(int, opts['frameGroupStride'])): + frames.extend(list(range( + cast(int, opts['frameBase']) + fds * cast(int, opts['frameStride']), + numFrames, + cast(int, opts['frameStride']) * cast(int, opts['frameGroupStride'])))) + numFrames = len(frames) + # check if numFrames zero and return early? + fw, fh, fhorz, fperframe, textures = _computeFramesPerTexture( + opts, numFrames, sizeX, sizeY) + # used area of each tile + usedw = int(math.floor(sizeX / max(sizeX / fw, sizeY / fh))) + usedh = int(math.floor(sizeY / max(sizeX / fw, sizeY / fh))) + # get the set of texture images + status: Dict[str, Any] = { + 'metadata': metadata, + 'options': opts, + 'src': [], + 'quads': [], + 'quadsToIdx': [], + 'frames': frames, + 'framesToIdx': {}, + } + if metadata.get('tileWidth') and metadata.get('tileHeight'): + # report that tiles below this level are not needed + status['minLevel'] = int(math.ceil(math.log(min( + usedw / metadata['tileWidth'], usedh / metadata['tileHeight'])) / math.log(2))) + status['framesToIdx'] = {frame: idx for idx, frame in enumerate(frames)} + for idx in range(textures): + frameList = frames[idx * fperframe: (idx + 1) * fperframe] + tfparams: Dict[str, Any] = { + 'framesAcross': fhorz, + 'width': fw, + 'height': fh, + 'fill': 'corner:black', + 'exact': False, + } + if len(frameList) != len(metadata.get('frames', [])): + tfparams['frameList'] = frameList + tfparams.update(cast(Dict[str, Any], opts['format'])) + tfparams.update(cast(Dict[str, Any], opts['query'])) + status['src'].append(tfparams) + f = len(frameList) + ivert = int(math.ceil(f / fhorz)) + ihorz = int(min(f, fhorz)) + for fidx in range(f): + quad = { + # z = -1 to place under other tile layers + 'ul': {'x': 0, 'y': 0, 'z': -1}, + # y coordinate is inverted + 'lr': {'x': sizeX, 'y': -sizeY, 'z': -1}, + 'crop': { + 'x': sizeX, + 'y': sizeY, + 'left': (fidx % ihorz) * fw, + 'top': (ivert - (fidx // ihorz)) * fh - usedh, + 'right': (fidx % ihorz) * fw + usedw, + 'bottom': (ivert - (fidx // ihorz)) * fh, + }, + } + status['quads'].append(quad) + status['quadsToIdx'].append(idx) + return status
+ + + +_recentThresholds: Dict[Tuple, Any] = {} + + +
+[docs] +def histogramThreshold(histogram: Dict[str, Any], threshold: float, fromMax: bool = False) -> float: + """ + Given a histogram and a threshold on a scale of [0, 1], return the bin + edge that excludes no more than the specified threshold amount of values. + For instance, a threshold of 0.02 would exclude at most 2% of the values. + + :param histogram: a histogram record for a specific channel. + :param threshold: a value from 0 to 1. + :param fromMax: if False, return values excluding the low end of the + histogram; if True, return values from excluding the high end of the + histogram. + :returns: the value the excludes no more than the threshold from the + specified end. + """ + key = (id(histogram), threshold, fromMax) + if key in _recentThresholds: + return _recentThresholds[key] + hist = histogram['hist'] + edges = histogram['bin_edges'] + samples = histogram['samples'] if not histogram.get('density') else 1 + if fromMax: + hist = hist[::-1] + edges = edges[::-1] + tally = 0 + result = edges[-1] + for idx in range(len(hist)): + if tally + hist[idx] > threshold * samples: + if not idx: + result = histogram['min' if not fromMax else 'max'] + else: + result = edges[idx] + break + tally += hist[idx] + if len(_recentThresholds) > 100: + _recentThresholds.clear() + _recentThresholds[key] = result + return result
+ + + +
+[docs] +def addPILFormatsToOutputOptions() -> None: + """ + Check PIL for available formats that be saved and add them to the lists of + of available formats. + """ + # Call this to actual register the extensions + PIL.Image.registered_extensions() + for key, value in PIL.Image.MIME.items(): + # We don't support these formats; ICNS and ICO have fixed sizes; PALM + # and PDF can't be read back by PIL without extensions + if key in {'ICNS', 'ICO', 'PALM', 'PDF'}: + continue + if key not in TileOutputMimeTypes and key in PIL.Image.SAVE: + TileOutputMimeTypes[key] = value + for key, value in PIL.Image.registered_extensions().items(): + key = key.lstrip('.') + if (key not in TileOutputMimeTypes and value in TileOutputMimeTypes and + key not in TileOutputPILFormat): + TileOutputPILFormat[key] = value
+ + + +addPILFormatsToOutputOptions() +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_converter.html b/_modules/large_image_converter.html new file mode 100644 index 000000000..c54cc95e3 --- /dev/null +++ b/_modules/large_image_converter.html @@ -0,0 +1,1165 @@ + + + + + + large_image_converter — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_converter

+import concurrent.futures
+import datetime
+import fractions
+import json
+import logging
+import math
+import os
+import re
+import struct
+import threading
+import time
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+from tempfile import TemporaryDirectory
+
+import numpy as np
+import tifftools
+
+import large_image
+from large_image.tilesource.utilities import (_gdalParameters,
+                                              _newFromFileLock, _vipsCast,
+                                              _vipsParameters)
+
+from . import format_aperio
+
+pyvips = None
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+logger = logging.getLogger('large-image-converter')
+
+
+FormatModules = {
+    'aperio': format_aperio,
+}
+
+# Estimated maximum memory use per frame conversion.  Used to limit concurrent
+# frame conversions.
+FrameMemoryEstimate = 3 * 1024 ** 3
+
+
+def _use_associated_image(key, **kwargs):
+    """
+    Check if an associated image key should be used.  If a list of images to
+    keep was specified, it must match at least one of the regex in that list.
+    If a list of images to exclude was specified, it must not any regex in that
+    list.  The exclude list takes priority.
+    """
+    if kwargs.get('_exclude_associated'):
+        for exp in kwargs['_exclude_associated']:
+            if re.match(exp, key):
+                return False
+    if kwargs.get('_keep_associated'):
+        for exp in kwargs['_keep_associated']:
+            if re.match(exp, key):
+                return True
+        return False
+    return True
+
+
+def _data_from_large_image(path, outputPath, **kwargs):
+    """
+    Check if the input file can be read by installed large_image tile sources.
+    If so, return the metadata, internal metadata, and extract each associated
+    image.
+
+    :param path: path of the file.
+    :param outputPath: the name of a temporary output file.
+    :returns: a dictionary of metadata, internal_metadata, and images.  images
+        is a dictionary of keys and paths.  Returns None if the path is not
+        readable by large_image.
+    """
+    _import_pyvips()
+    if not path.startswith('large_image://test'):
+        try:
+            ts = large_image.open(path, noCache=True)
+        except Exception:
+            return
+    else:
+        import urllib.parse
+
+        tsparams = {
+            k: int(v[0]) if v[0].isdigit() else v[0]
+            for k, v in urllib.parse.parse_qs(
+                path.split('?', 1)[1] if '?' in path else '').items()}
+        ts = large_image.open('large_image://test', **tsparams)
+    results = {
+        'metadata': ts.getMetadata(),
+        'internal_metadata': ts.getInternalMetadata(),
+        'images': {},
+        'tilesource': ts,
+    }
+    tasks = []
+    pool = _get_thread_pool(**kwargs)
+    for key in ts.getAssociatedImagesList():
+        if not _use_associated_image(key, **kwargs):
+            continue
+        try:
+            img, mime = ts.getAssociatedImage(key)
+        except Exception:
+            continue
+        savePath = outputPath + '-%s-%s.tiff' % (key, time.strftime('%Y%m%d-%H%M%S'))
+        # TODO: allow specifying quality separately from main image quality
+        _pool_add(tasks, (pool.submit(
+            _convert_via_vips, img, savePath, outputPath, mime=mime, forTiled=False), ))
+        results['images'][key] = savePath
+    _drain_pool(pool, tasks, 'associated images')
+    return results
+
+
+def _generate_geotiff(inputPath, outputPath, **kwargs):
+    """
+    Take a source input file, readable by gdal, and output a cloud-optimized
+    geotiff file.  See https://gdal.org/drivers/raster/cog.html.
+
+    :param inputPath: the path to the input file or base file of a set.
+    :param outputPath: the path of the output file.
+    Optional parameters that can be specified in kwargs:
+    :param tileSize: the horizontal and vertical tile size.
+    :param compression: one of 'jpeg', 'deflate' (zip), 'lzw', or 'zstd'.
+    :param quality: a jpeg quality passed to vips.  0 is small, 100 is high
+        quality.  90 or above is recommended.
+    :param level: compression level for zstd, 1-22 (default is 10).
+    :param predictor: one of 'none', 'horizontal', 'float', or 'yes' used for
+        lzw and deflate.
+    """
+    from osgeo import gdal, gdalconst
+
+    cmdopt = _gdalParameters(**kwargs)
+    cmd = ['gdal_translate', inputPath, outputPath] + cmdopt
+    logger.info('Convert to geotiff: %r', cmd)
+    try:
+        # subprocess.check_call(cmd)
+        ds = gdal.Open(inputPath, gdalconst.GA_ReadOnly)
+        gdal.Translate(outputPath, ds, options=cmdopt)
+    except Exception:
+        os.unlink(outputPath)
+        raise
+
+
+def _generate_multiframe_tiff(inputPath, outputPath, tempPath, lidata, **kwargs):
+    """
+    Take a source input file with multiple frames and output a multi-pyramidal
+    tiff file.
+
+    :param inputPath: the path to the input file or base file of a set.
+    :param outputPath: the path of the output file.
+    :param tempPath: a temporary file in a temporary directory.
+    :param lidata: data from a large_image tilesource including associated
+        images.
+    Optional parameters that can be specified in kwargs:
+    :param tileSize: the horizontal and vertical tile size.
+    :param compression: one of 'jpeg', 'deflate' (zip), 'lzw', 'packbits',
+        'zstd', or 'jp2k'.
+    :param quality: a jpeg quality passed to vips.  0 is small, 100 is high
+        quality.  90 or above is recommended.
+    :param level: compression level for zstd, 1-22 (default is 10).
+    :param predictor: one of 'none', 'horizontal', or 'float' used for lzw and
+        deflate.
+    """
+    _import_pyvips()
+
+    with _newFromFileLock:
+        image = pyvips.Image.new_from_file(inputPath)
+    width = image.width
+    height = image.height
+    pages = 1
+    if 'n-pages' in image.get_fields():
+        pages = image.get_value('n-pages')
+    # Now check if there are other images we need to convert or preserve
+    outputList = []
+    imageSizes = []
+    tasks = []
+    pool = _get_thread_pool(memoryLimit=FrameMemoryEstimate, **kwargs)
+    onlyFrame = int(kwargs.get('onlyFrame')) if str(kwargs.get('onlyFrame')).isdigit() else None
+    frame = 0
+    # Process each image separately to pyramidize it
+    for page in range(pages):
+        subInputPath = inputPath + '[page=%d]' % page
+        with _newFromFileLock:
+            subImage = pyvips.Image.new_from_file(subInputPath)
+        imageSizes.append((subImage.width, subImage.height, subInputPath, page))
+        if subImage.width != width or subImage.height != height:
+            if subImage.width * subImage.height <= width * height:
+                continue
+            logger.info('Bigger image found (was %dx%d, now %dx%d)',
+                        width, height, subImage.width, subImage.height)
+            for path in outputList:
+                os.unlink(path)
+            width = subImage.width
+            height = subImage.height
+        frame += 1
+        if onlyFrame is not None and onlyFrame + 1 != frame:
+            continue
+        subOutputPath = tempPath + '-%d-%s.tiff' % (
+            page + 1, time.strftime('%Y%m%d-%H%M%S'))
+        _pool_add(tasks, (pool.submit(
+            _convert_via_vips, subInputPath, subOutputPath, tempPath,
+            status='%d/%d' % (page, pages), **kwargs), ))
+        outputList.append(subOutputPath)
+    extraImages = {}
+    if not lidata or not len(lidata['images']):
+        # If we couldn't extract images from li, try to detect non-primary
+        # images from the original file.  These are any images who size is
+        # not a power of two division of the primary image size
+        possibleSizes = _list_possible_sizes(width, height)
+        for w, h, subInputPath, page in imageSizes:
+            if (w, h) not in possibleSizes:
+                key = 'image_%d' % page
+                if not _use_associated_image(key, **kwargs):
+                    continue
+                savePath = tempPath + '-%s-%s.tiff' % (key, time.strftime('%Y%m%d-%H%M%S'))
+                _pool_add(tasks, (pool.submit(
+                    _convert_via_vips, subInputPath, savePath, tempPath, False), ))
+                extraImages[key] = savePath
+    _drain_pool(pool, tasks, 'subpage')
+    _output_tiff(outputList, outputPath, tempPath, lidata, extraImages, **kwargs)
+
+
+def _generate_tiff(inputPath, outputPath, tempPath, lidata, **kwargs):
+    """
+    Take a source input file, readable by vips, and output a pyramidal tiff
+    file.
+
+    :param inputPath: the path to the input file or base file of a set.
+    :param outputPath: the path of the output file.
+    :param tempPath: a temporary file in a temporary directory.
+    :param lidata: data from a large_image tilesource including associated
+        images.
+    Optional parameters that can be specified in kwargs:
+    :param tileSize: the horizontal and vertical tile size.
+    :param compression: one of 'jpeg', 'deflate' (zip), 'lzw', 'packbits',
+        'zstd', or 'jp2k'.
+    :param quality: a jpeg quality passed to vips.  0 is small, 100 is high
+        quality.  90 or above is recommended.
+    :param level: compression level for zstd, 1-22 (default is 10).
+    :param predictor: one of 'none', 'horizontal', or 'float' used for lzw and
+        deflate.
+    """
+    _import_pyvips()
+    subOutputPath = tempPath + '-%s.tiff' % (time.strftime('%Y%m%d-%H%M%S'))
+    _convert_via_vips(inputPath, subOutputPath, tempPath, **kwargs)
+    _output_tiff([subOutputPath], outputPath, tempPath, lidata, **kwargs)
+
+
+def _convert_via_vips(inputPathOrBuffer, outputPath, tempPath, forTiled=True,
+                      status=None, **kwargs):
+    """
+    Convert a file, buffer, or vips image to a tiff file.  This is equivalent
+    to a vips command line of
+      vips tiffsave <input path> <output path>
+    followed by the convert params in the form of --<key>[=<value>] where no
+    value needs to be specified if they are True.
+
+    :param inputPathOrBuffer: a file path, bytes object, or vips image.
+    :param outputPath: the name of the file to save.
+    :param tempPath: a directory where temporary files may be stored.  vips
+        also stores files in TMPDIR
+    :param forTiled: True if the output should be tiled, false if not.
+    :param status: an optional additional string to add to log messages.
+    :param kwargs: addition arguments that get passed to _vipsParameters
+        and _convert_to_jp2k.
+    """
+    _import_pyvips()
+    convertParams = _vipsParameters(forTiled, **kwargs)
+    status = (', ' + status) if status else ''
+    if isinstance(inputPathOrBuffer, pyvips.vimage.Image):
+        source = 'vips image'
+        image = inputPathOrBuffer
+    elif isinstance(inputPathOrBuffer, bytes):
+        source = 'buffer'
+        image = pyvips.Image.new_from_buffer(inputPathOrBuffer, '')
+    else:
+        source = inputPathOrBuffer
+        with _newFromFileLock:
+            image = pyvips.Image.new_from_file(inputPathOrBuffer)
+    logger.info('Input: %s, Output: %s, Options: %r%s',
+                source, outputPath, convertParams, status)
+    image = image.autorot()
+    adjusted = format_hook('modify_vips_image_before_output', image, convertParams, **kwargs)
+    if adjusted is False:
+        return
+    elif adjusted:
+        image = adjusted
+    if (convertParams['compression'] not in {'jpeg'} or
+            image.interpretation != pyvips.Interpretation.SCRGB):
+        # jp2k compression supports more than 8-bits per sample, but the
+        # decompressor claims this is unsupported.
+        image = _vipsCast(
+            image,
+            convertParams['compression'] in {'webp', 'jpeg'} or
+            kwargs.get('compression') in {'jp2k'})
+    # TODO: revisit the TMPDIR override; this is not thread safe
+    # oldtmpdir = os.environ.get('TMPDIR')
+    # os.environ['TMPDIR'] = os.path.dirname(tempPath)
+    # try:
+    #     image.write_to_file(outputPath, **convertParams)
+    # finally:
+    #     if oldtmpdir is not None:
+    #         os.environ['TMPDIR'] = oldtmpdir
+    #     else:
+    #         del os.environ['TMPDIR']
+    image.write_to_file(outputPath, **convertParams)
+    if kwargs.get('compression') == 'jp2k':
+        _convert_to_jp2k(outputPath, **kwargs)
+
+
+def _convert_to_jp2k_tile(lock, fptr, dest, offset, length, shape, dtype, jp2kargs):
+    """
+    Read an uncompressed tile from a file and save it as a JP2000 file.
+
+    :param lock: a lock to ensure exclusive access to the file.
+    :param fptr: a pointer to the open file.
+    :param dest: the output path for the jp2k file.
+    :param offset: the location in the input file with the data.
+    :param length: the number of bytes to read.
+    :param shape: a tuple with the shape of the tile to read.  This is usually
+        (height, width, channels).
+    :param dtype: the numpy dtype of the data in the tile.
+    :param jp2kargs: arguments to pass to the compression, such as psnr or
+        cratios.
+    """
+    import glymur
+
+    with lock:
+        fptr.seek(offset)
+        data = fptr.read(length)
+    data = np.frombuffer(data, dtype=dtype)
+    data = np.reshape(data, shape)
+    glymur.Jp2k(dest, data=data, **jp2kargs)
+
+
+def _concurrency_to_value(_concurrency=None, **kwargs):
+    """
+    Convert the _concurrency value to a number of cpus.
+
+    :param _concurrency: a positive value for a set number of cpus.  <= 0 for
+        the number of logical cpus less that amount.  None is the same as 0.
+    :returns: the number of cpus.
+    """
+    _concurrency = int(_concurrency) if str(_concurrency).isdigit() else 0
+    if _concurrency > 0:
+        return _concurrency
+    return max(1, large_image.config.cpu_count(logical=True) + _concurrency)
+
+
+def _get_thread_pool(memoryLimit=None, parentConcurrency=None, numItems=None, **kwargs):
+    """
+    Allocate a thread pool based on the specific concurrency.
+
+    :param memoryLimit: if not None, limit the concurrency to no more than one
+        process per memoryLimit bytes of total memory.
+    """
+    concurrency = _concurrency_to_value(**kwargs)
+    if parentConcurrency and parentConcurrency > 1 and concurrency > 1:
+        concurrency = max(1, int(math.ceil(concurrency / parentConcurrency)))
+    if memoryLimit:
+        if parentConcurrency:
+            memoryLimit *= parentConcurrency
+        concurrency = min(concurrency, large_image.config.total_memory() // memoryLimit)
+    if numItems and numItems >= 1 and concurrency > numItems:
+        concurrency = numItems
+    concurrency = max(1, concurrency)
+    return concurrent.futures.ThreadPoolExecutor(max_workers=concurrency)
+
+
+def _pool_log(left, total, label):
+    """
+    Log processing within a pool.
+
+    :param left: units left to process.
+    :param total: total units left to process.
+    :param label: label to log describing what is being processed.
+    """
+    if not hasattr(logger, '_pool_log_starttime'):
+        logger._pool_log_starttime = time.time()
+    if not hasattr(logger, '_pool_log_lastlog'):
+        logger._pool_log_lastlog = time.time()
+    if time.time() - logger._pool_log_lastlog < 10:
+        return
+    elapsed = time.time() - logger._pool_log_starttime
+    logger.debug('%d/%d %s left %4.2fs', left, total, label, elapsed)
+    logger._pool_log_lastlog = time.time()
+
+
+def _pool_add(tasks, newtask):
+    """
+    Add a new task to a pool, then drain any finished tasks at the start of the
+    pool.
+
+    :param tasks: a list containing either lists or tuples, the last element
+        of which is a task submitted to the pool.  Altered.
+    :param newtask: a list or tuple to add to the pool.
+    """
+    tasks.append(newtask)
+    while len(tasks):
+        try:
+            tasks[0][-1].result(0)
+        except concurrent.futures.TimeoutError:
+            break
+        tasks.pop(0)
+
+
+def _drain_pool(pool, tasks, label=''):
+    """
+    Wait for all tasks in a pool to complete, then shutdown the pool.
+
+    :param pool: a concurrent futures pool.
+    :param tasks: a list containing either lists or tuples, the last element
+        of which is a task submitted to the pool.  Altered.
+    """
+    numtasks = len(tasks)
+    _pool_log(len(tasks), numtasks, label)
+    while len(tasks):
+        # This allows better stopping on a SIGTERM
+        try:
+            tasks[0][-1].result(0.1)
+        except concurrent.futures.TimeoutError:
+            continue
+        tasks.pop(0)
+        _pool_log(len(tasks), numtasks, label)
+    pool.shutdown(False)
+
+
+def _convert_to_jp2k(path, **kwargs):
+    """
+    Given a tiled tiff file without compression, convert it to jp2k compression
+    using the gylmur library.  This expects a tiff as written by vips without
+    any subifds.
+
+    :param path: the path of the tiff file.  The file is altered.
+    :param psnr: if set, the target psnr.  0 for lossless.
+    :param cr: is set, the target compression ratio.  1 for lossless.
+    """
+    info = tifftools.read_tiff(path)
+    jp2kargs = {}
+    if 'psnr' in kwargs:
+        jp2kargs['psnr'] = [int(kwargs['psnr'])]
+    elif 'cr' in kwargs:
+        jp2kargs['cratios'] = [int(kwargs['cr'])]
+    tilecount = sum(len(ifd['tags'][tifftools.Tag.TileOffsets.value]['data'])
+                    for ifd in info['ifds'])
+    processed = 0
+    lastlog = 0
+    tasks = []
+    lock = threading.Lock()
+    pool = _get_thread_pool(**kwargs)
+    with open(path, 'r+b') as fptr:
+        for ifd in info['ifds']:
+            ifd['tags'][tifftools.Tag.Compression.value]['data'][0] = (
+                tifftools.constants.Compression.JP2000)
+            shape = (
+                ifd['tags'][tifftools.Tag.TileWidth.value]['data'][0],
+                ifd['tags'][tifftools.Tag.TileLength.value]['data'][0],
+                len(ifd['tags'][tifftools.Tag.BitsPerSample.value]['data']))
+            dtype = np.uint16 if ifd['tags'][
+                tifftools.Tag.BitsPerSample.value]['data'][0] == 16 else np.uint8
+            for idx, offset in enumerate(ifd['tags'][tifftools.Tag.TileOffsets.value]['data']):
+                tmppath = path + '%d.jp2k' % processed
+                tasks.append((ifd, idx, processed, tmppath, pool.submit(
+                    _convert_to_jp2k_tile, lock, fptr, tmppath, offset,
+                    ifd['tags'][tifftools.Tag.TileByteCounts.value]['data'][idx],
+                    shape, dtype, jp2kargs)))
+                processed += 1
+        while len(tasks):
+            try:
+                tasks[0][-1].result(0.1)
+            except concurrent.futures.TimeoutError:
+                continue
+            ifd, idx, processed, tmppath, task = tasks.pop(0)
+            data = open(tmppath, 'rb').read()
+            os.unlink(tmppath)
+            # Remove first comment marker.  It adds needless bytes
+            compos = data.find(b'\xff\x64')
+            if compos >= 0 and compos + 4 < len(data):
+                comlen = struct.unpack('>H', data[compos + 2:compos + 4])[0]
+                if compos + 2 + comlen + 1 < len(data) and data[compos + 2 + comlen] == 0xff:
+                    data = data[:compos] + data[compos + 2 + comlen:]
+            with lock:
+                fptr.seek(0, os.SEEK_END)
+                ifd['tags'][tifftools.Tag.TileOffsets.value]['data'][idx] = fptr.tell()
+                ifd['tags'][tifftools.Tag.TileByteCounts.value]['data'][idx] = len(data)
+                fptr.write(data)
+            if time.time() - lastlog >= 10 and tilecount > 1:
+                logger.debug('Converted %d of %d tiles to jp2k', processed + 1, tilecount)
+                lastlog = time.time()
+        pool.shutdown(False)
+        fptr.seek(0, os.SEEK_END)
+        for ifd in info['ifds']:
+            ifd['size'] = fptr.tell()
+        info['size'] = fptr.tell()
+    tmppath = path + '.jp2k.tiff'
+    tifftools.write_tiff(info, tmppath, bigtiff=False, allowExisting=True)
+    os.unlink(path)
+    os.rename(tmppath, path)
+
+
+def _convert_large_image_tile(tilelock, strips, tile):
+    """
+    Add a single tile to a list of strips for a vips image so that they can be
+    composited together.
+
+    :param tilelock: a lock for thread safety.
+    :param strips: an array of strips to adds to the final vips image.
+    :param tile: a tileIterator tile.
+    """
+    data = tile['tile']
+    if data.dtype.char not in large_image.constants.dtypeToGValue:
+        data = data.astype('d')
+    vimg = pyvips.Image.new_from_memory(
+        np.ascontiguousarray(data).data,
+        data.shape[1], data.shape[0], data.shape[2],
+        large_image.constants.dtypeToGValue[data.dtype.char])
+    vimgTemp = pyvips.Image.new_temp_file('%s.v')
+    vimg.write(vimgTemp)
+    vimg = vimgTemp
+    x = tile['x']
+    ty = tile['tile_position']['level_y']
+    with tilelock:
+        while len(strips) <= ty:
+            strips.append(None)
+        if strips[ty] is None:
+            strips[ty] = vimg
+            if not x:
+                return
+        if vimg.bands > strips[ty].bands:
+            vimg = vimg[:strips[ty].bands]
+        elif strips[ty].bands > vimg.bands:
+            strips[ty] = strips[ty][:vimg.bands]
+        strips[ty] = strips[ty].insert(vimg, x, 0, expand=True)
+
+
+def _convert_large_image_frame(frame, numFrames, ts, frameOutputPath, tempPath,
+                               parentConcurrency=None, **kwargs):
+    """
+    Convert a single frame from a large_image source.  This parallelizes tile
+    reads.  Once all tiles are converted to a composited vips image, a tiff
+    file is generated.
+
+    :param frame: the 0-based frame number.
+    :param numFrames: the total number of frames; used for logging.
+    :param ts: the open tile source.
+    :param frameOutputPath: the destination name for the tiff file.
+    :param tempPath: a temporary file in a temporary directory.
+    :param parentConcurrency: amount of concurrency used by parent task.
+    """
+    # The iterator tile size is a balance between memory use and fewer calls
+    # and file handles.
+    _iterTileSize = 4096
+    logger.info('Processing frame %d/%d', frame + 1, numFrames)
+    strips = []
+    pool = _get_thread_pool(
+        memoryLimit=FrameMemoryEstimate,
+        # allow multiple tiles even if we are using all the cores, as it
+        # balances I/O and computation
+        parentConcurrency=(parentConcurrency // 2),
+        **kwargs)
+    tasks = []
+    tilelock = threading.Lock()
+    for tile in ts.tileIterator(tile_size=dict(width=_iterTileSize), frame=frame):
+        _pool_add(tasks, (pool.submit(_convert_large_image_tile, tilelock, strips, tile), ))
+    _drain_pool(pool, tasks, f'tiles from frame {frame + 1}/{numFrames}')
+    minbands = min(strip.bands for strip in strips)
+    maxbands = max(strip.bands for strip in strips)
+    if minbands != maxbands:
+        strips = [strip[:minbands] for strip in strips]
+    img = strips[0]
+    for stripidx in range(1, len(strips)):
+        img = img.insert(strips[stripidx], 0, stripidx * _iterTileSize, expand=True)
+    _convert_via_vips(
+        img, frameOutputPath, tempPath, status='%d/%d' % (frame + 1, numFrames), **kwargs)
+
+
+def _convert_large_image(inputPath, outputPath, tempPath, lidata, **kwargs):
+    """
+    Take a large_image source and convert it by resaving each tiles image with
+    vips.
+
+    :param inputPath: the path to the input file or base file of a set.
+    :param outputPath: the path of the output file.
+    :param tempPath: a temporary file in a temporary directory.
+    :param lidata: data from a large_image tilesource including associated
+        images.
+    """
+    ts = lidata['tilesource']
+    numFrames = len(lidata['metadata'].get('frames', [0]))
+    outputList = []
+    tasks = []
+    startFrame = 0
+    endFrame = numFrames
+    if kwargs.get('onlyFrame') is not None and str(kwargs.get('onlyFrame')):
+        startFrame = int(kwargs.get('onlyFrame'))
+        endFrame = startFrame + 1
+    pool = _get_thread_pool(memoryLimit=FrameMemoryEstimate,
+                            numItems=endFrame - startFrame, **kwargs)
+    for frame in range(startFrame, endFrame):
+        frameOutputPath = tempPath + '-%d-%s.tiff' % (
+            frame + 1, time.strftime('%Y%m%d-%H%M%S'))
+        _pool_add(tasks, (pool.submit(
+            _convert_large_image_frame, frame, numFrames, ts, frameOutputPath,
+            tempPath, pool._max_workers, **kwargs), ))
+        outputList.append(frameOutputPath)
+    _drain_pool(pool, tasks, 'frames')
+    _output_tiff(outputList, outputPath, tempPath, lidata, **kwargs)
+
+
+def _output_tiff(inputs, outputPath, tempPath, lidata, extraImages=None, **kwargs):
+    """
+    Given a list of input tiffs and data as parsed by _data_from_large_image,
+    generate an output tiff file with the associated images, correct scale, and
+    other metadata.
+
+    :param inputs: a list of pyramidal input files.
+    :param outputPath: the final destination.
+    :param tempPath: a temporary file in a temporary directory.
+    :param lidata: large_image data including metadata and associated images.
+    :param extraImages: an optional dictionary of keys and paths to add as
+        extra associated images.
+    """
+    logger.debug('Reading %s', inputs[0])
+    info = tifftools.read_tiff(inputs[0])
+    ifdIndices = [0]
+    imgDesc = info['ifds'][0]['tags'].get(tifftools.Tag.ImageDescription.value)
+    description = _make_li_description(
+        len(info['ifds']), len(inputs), lidata,
+        (len(extraImages) if extraImages else 0) + (len(lidata['images']) if lidata else 0),
+        imgDesc['data'] if imgDesc else None, **kwargs)
+    info['ifds'][0]['tags'][tifftools.Tag.ImageDescription.value] = {
+        'data': description,
+        'datatype': tifftools.Datatype.ASCII,
+    }
+    if lidata:
+        _set_resolution(info['ifds'], lidata['metadata'])
+    if len(inputs) > 1:
+        if kwargs.get('subifds') is not False:
+            info['ifds'][0]['tags'][tifftools.Tag.SubIFD.value] = {
+                'ifds': info['ifds'][1:],
+            }
+            info['ifds'][1:] = []
+        for idx, inputPath in enumerate(inputs):
+            if not idx:
+                continue
+            logger.debug('Reading %s', inputPath)
+            nextInfo = tifftools.read_tiff(inputPath)
+            if lidata:
+                _set_resolution(nextInfo['ifds'], lidata['metadata'])
+                if len(lidata['metadata'].get('frames', [])) > idx:
+                    nextInfo['ifds'][0]['tags'][tifftools.Tag.ImageDescription.value] = {
+                        'data': json.dumps(
+                            {'frame': lidata['metadata']['frames'][idx]},
+                            separators=(',', ':'), sort_keys=True, default=json_serial),
+                        'datatype': tifftools.Datatype.ASCII,
+                    }
+            ifdIndices.append(len(info['ifds']))
+            if kwargs.get('subifds') is not False:
+                nextInfo['ifds'][0]['tags'][tifftools.Tag.SubIFD.value] = {
+                    'ifds': nextInfo['ifds'][1:],
+                }
+                info['ifds'].append(nextInfo['ifds'][0])
+            else:
+                info['ifds'].extend(nextInfo['ifds'])
+    ifdIndices.append(len(info['ifds']))
+    assocList = []
+    if lidata:
+        assocList += list(lidata['images'].items())
+    if extraImages:
+        assocList += list(extraImages.items())
+    for key, assocPath in assocList:
+        assocInfo = tifftools.read_tiff(assocPath)
+        assocInfo['ifds'][0]['tags'][tifftools.Tag.ImageDescription.value] = {
+            'data': key,
+            'datatype': tifftools.Datatype.ASCII,
+        }
+        info['ifds'] += assocInfo['ifds']
+    if format_hook('modify_tiff_before_write', info, ifdIndices, tempPath,
+                   lidata, **kwargs) is False:
+        return
+    logger.debug('Writing %s', outputPath)
+    tifftools.write_tiff(info, outputPath, bigEndian=False, bigtiff=False, allowExisting=True)
+
+
+def _set_resolution(ifds, metadata):
+    """
+    Given metadata with a scale in mm_x and/or mm_y, set the resolution for
+    each ifd, assuming that each one is half the scale of the previous one.
+
+    :param ifds: a list of ifds from a single pyramid.  The resolution may be
+        set on each one.
+    :param metadata: metadata with a scale specified by mm_x and/or mm_y.
+    """
+    if metadata.get('mm_x') or metadata.get('mm_y'):
+        for idx, ifd in enumerate(ifds):
+            ifd['tags'][tifftools.Tag.ResolutionUnit.value] = {
+                'data': [tifftools.constants.ResolutionUnit.Centimeter],
+                'datatype': tifftools.Datatype.SHORT,
+            }
+            for mkey, tkey in (('mm_x', 'XResolution'), ('mm_y', 'YResolution')):
+                if metadata[mkey]:
+                    val = fractions.Fraction(
+                        10.0 / (metadata[mkey] * 2 ** idx)).limit_denominator()
+                    if val.numerator >= 2**32 or val.denominator >= 2**32:
+                        origval = val
+                        denom = 1000000
+                        while val.numerator >= 2**32 or val.denominator >= 2**32 and denom > 1:
+                            denom = int(denom / 10)
+                            val = origval.limit_denominator(denom)
+                    if val.numerator >= 2**32 or val.denominator >= 2**32:
+                        continue
+                    ifd['tags'][tifftools.Tag[tkey].value] = {
+                        'data': [val.numerator, val.denominator],
+                        'datatype': tifftools.Datatype.RATIONAL,
+                    }
+
+
+def _import_pyvips():
+    """
+    Import pyvips on demand.
+    """
+    global pyvips
+
+    if pyvips is None:
+        import pyvips
+
+
+def _is_eightbit(path, tiffinfo=None):
+    """
+    Check if a path has an unsigned 8-bit per sample data size.  If any known
+    channel is otherwise or this is unknown, this returns False.
+
+    :param path: The path to the file
+    :param tiffinfo: data extracted from tifftools.read_tiff(path).
+    :returns: True if known to be 8 bits per sample.
+    """
+    if not tiffinfo:
+        return False
+    try:
+        if (tifftools.Tag.SampleFormat.value in tiffinfo['ifds'][0]['tags'] and
+                not all(val == tifftools.constants.SampleFormat.uint for val in
+                        tiffinfo['ifds'][0]['tags'][tifftools.Tag.SampleFormat.value]['data'])):
+            return False
+        if tifftools.Tag.BitsPerSample.value in tiffinfo['ifds'][0]['tags'] and not all(
+                val == 8 for val in
+                tiffinfo['ifds'][0]['tags'][tifftools.Tag.BitsPerSample.value]['data']):
+            return False
+    except Exception:
+        return False
+    return True
+
+
+def _is_lossy(path, tiffinfo=None):
+    """
+    Check if a path uses lossy compression.  This imperfectly just checks if
+    the file is a TIFF and stored in one of the JPEG formats.
+
+    :param path: The path to the file
+    :param tiffinfo: data extracted from tifftools.read_tiff(path).
+    :returns: True if known to be lossy.
+    """
+    if not tiffinfo:
+        return False
+    try:
+        return bool(tifftools.constants.Compression[
+            tiffinfo['ifds'][0]['tags'][
+                tifftools.Tag.Compression.value]['data'][0]].lossy)
+    except Exception:
+        return False
+
+
+def _is_multiframe(path):
+    """
+    Check if a path is a multiframe file.
+
+    :param path: The path to the file
+    :returns: True if multiframe.
+    """
+    _import_pyvips()
+    try:
+        with _newFromFileLock:
+            image = pyvips.Image.new_from_file(path)
+    except Exception:
+        try:
+            open(path, 'rb').read(1)
+            raise
+        except Exception:
+            logger.warning('Is the file reachable and readable? (%r)', path)
+            raise OSError(path) from None
+    pages = 1
+    if 'n-pages' in image.get_fields():
+        pages = image.get_value('n-pages')
+    return pages > 1
+
+
+def _list_possible_sizes(width, height):
+    """
+    Given a width and height, return a list of possible sizes that could be
+    reasonable powers-of-two smaller versions of that size.  This includes
+    the values rounded up and down.
+    """
+    results = [(width, height)]
+    pos = 0
+    while pos < len(results):
+        w, h = results[pos]
+        if w > 1 or h > 1:
+            w2f = int(math.floor(w / 2))
+            h2f = int(math.floor(h / 2))
+            w2c = int(math.ceil(w / 2))
+            h2c = int(math.ceil(h / 2))
+            for w2, h2 in [(w2f, h2f), (w2f, h2c), (w2c, h2f), (w2c, h2c)]:
+                if (w2, h2) not in results:
+                    results.append((w2, h2))
+        pos += 1
+    return results
+
+
+
+[docs] +def json_serial(obj): + """ + Fallback serializier for json. This serializes datetime objects to iso + format. + + :param obj: an object to serialize. + :returns: a serialized string. + """ + if isinstance(obj, (datetime.datetime, datetime.date)): + return obj.isoformat() + return str(obj)
+ + + +def _make_li_description( + framePyramidSize, numFrames, lidata=None, numAssociatedImages=0, + imageDescription=None, **kwargs): + """ + Given the number of frames, the number of levels per frame, the associated + image list, and any metadata from large_image, construct a json string with + information about the whole image. + + :param framePyramidSize: the number of layers per frame. + :param numFrames: the number of frames. + :param lidata: the data returned from _data_from_large_image. + :param numAssociatedImages: the number of associated images. + :param imageDescription: if present, the original description. + :returns: a json string + """ + results = { + 'large_image_converter': { + 'conversion_epoch': time.time(), + 'version': __version__, + 'levels': framePyramidSize, + 'frames': numFrames, + 'associated': numAssociatedImages, + 'arguments': { + k: v for k, v in kwargs.items() + if not k.startswith('_') and ('_' + k) not in kwargs and + k not in {'overwrite'}}, + }, + } + if lidata: + results['metadata'] = lidata['metadata'] + if len(lidata['metadata'].get('frames', [])) >= 1: + results['frame'] = lidata['metadata']['frames'][0] + if len(lidata['metadata'].get('channels', [])) >= 1: + results['channels'] = lidata['metadata']['channels'] + results['internal'] = lidata['internal_metadata'] + if imageDescription: + results['image_description'] = imageDescription + return json.dumps(results, separators=(',', ':'), sort_keys=True, default=json_serial) + + +
+[docs] +def format_hook(funcname, *args, **kwargs): + """ + Call a function specific to a file format. + + :param funcname: name of the function. + :param args: parameters to pass to the function. + :param kwargs: parameters to pass to the function. + :returns: dependent on the function. False to indicate no further + processing should be done. + """ + format = str(kwargs.get('format')).lower() + func = getattr(FormatModules.get(format, {}), funcname, None) + if callable(func): + return func(*args, **kwargs)
+ + + +
+[docs] +def convert(inputPath, outputPath=None, **kwargs): # noqa: C901 + """ + Take a source input file and output a pyramidal tiff file. + + :param inputPath: the path to the input file or base file of a set. + :param outputPath: the path of the output file. + + Optional parameters that can be specified in kwargs: + + :param tileSize: the horizontal and vertical tile size. + :param format: one of 'tiff' or 'aperio'. Default is 'tiff'. + :param onlyFrame: None for all frames or the 0-based frame number to just + convert a single frame of the source. + :param compression: one of 'jpeg', 'deflate' (zip), 'lzw', 'packbits', + 'zstd', or 'none'. + :param quality: a jpeg or webp quality passed to vips. 0 is small, 100 is + high quality. 90 or above is recommended. For webp, 0 is lossless. + :param level: compression level for zstd, 1-22 (default is 10) and deflate, + 1-9. + :param predictor: one of 'none', 'horizontal', 'float', or 'yes' used for + lzw and deflate. Default is horizontal for non-geospatial data and yes + for geospatial. + :param psnr: psnr value for jp2k, higher results in large files. 0 is + lossless. + :param cr: jp2k compression ratio. 1 is lossless, 100 will try to make + a file 1% the size of the original, etc. + :param subifds: if True (the default), when creating a multi-frame file, + store lower resolution tiles in sub-ifds. If False, store all data in + primary ifds. + :param overwrite: if not True, throw an exception if the output path + already exists. + + Additional optional parameters: + + :param geospatial: if not None, a boolean indicating if this file is + geospatial. If not specified or None, this will be checked. + :param _concurrency: the number of cpus to use during conversion. None to + use the logical cpu count. + + :returns: outputPath if successful + """ + logger._pool_log_starttime = time.time() + if kwargs.get('_concurrency'): + os.environ['VIPS_CONCURRENCY'] = str(_concurrency_to_value(**kwargs)) + geospatial = kwargs.get('geospatial') + if geospatial is None: + geospatial = is_geospatial(inputPath) + logger.debug('Is file geospatial: %r', geospatial) + suffix = format_hook('adjust_params', geospatial, kwargs, **kwargs) + if suffix is False: + return + suffix = suffix or ('.tiff' if not geospatial else '.geo.tiff') + if not outputPath: + outputPath = os.path.splitext(inputPath)[0] + suffix + if outputPath.endswith('.geo' + suffix): + outputPath = outputPath[:len(outputPath) - len(suffix) - 4] + suffix + if outputPath == inputPath: + outputPath = (os.path.splitext(inputPath)[0] + '.' + + time.strftime('%Y%m%d-%H%M%S') + suffix) + if os.path.exists(outputPath) and not kwargs.get('overwrite'): + msg = 'Output file already exists.' + raise Exception(msg) + try: + tiffinfo = tifftools.read_tiff(inputPath) + except Exception: + tiffinfo = None + eightbit = _is_eightbit(inputPath, tiffinfo) + if not kwargs.get('compression', None): + kwargs = kwargs.copy() + lossy = _is_lossy(inputPath, tiffinfo) + logger.debug('Is file lossy: %r', lossy) + logger.debug('Is file 8 bits per samples: %r', eightbit) + kwargs['_compression'] = None + kwargs['compression'] = 'jpeg' if lossy and eightbit else 'lzw' + if geospatial: + _generate_geotiff(inputPath, outputPath, eightbit=eightbit or None, **kwargs) + else: + with TemporaryDirectory() as tempDir: + tempPath = os.path.join(tempDir, os.path.basename(outputPath)) + lidata = _data_from_large_image(str(inputPath), tempPath, **kwargs) + logger.log(logging.DEBUG - 1, 'large_image information for %s: %r', + inputPath, lidata) + if lidata and (not is_vips( + inputPath, (lidata['metadata']['sizeX'], lidata['metadata']['sizeY'])) or ( + len(lidata['metadata'].get('frames', [])) >= 2 and + not _is_multiframe(inputPath))): + _convert_large_image(inputPath, outputPath, tempPath, lidata, **kwargs) + elif _is_multiframe(inputPath): + _generate_multiframe_tiff(inputPath, outputPath, tempPath, lidata, **kwargs) + else: + try: + _generate_tiff(inputPath, outputPath, tempPath, lidata, **kwargs) + except Exception: + if lidata: + _convert_large_image(inputPath, outputPath, tempPath, lidata, **kwargs) + return outputPath
+ + + +
+[docs] +def is_geospatial(path): + """ + Check if a path is likely to be a geospatial file. + + :param path: The path to the file + :returns: True if geospatial. + """ + try: + from osgeo import gdal, gdalconst + except ImportError: + logger.warning('Cannot import GDAL.') + return False + gdal.UseExceptions() + try: + ds = gdal.Open(path, gdalconst.GA_ReadOnly) + except Exception: + return False + if ds and ( + (ds.GetGCPs() and ds.GetGCPProjection()) or + ds.GetProjection() or + ds.GetDriver().ShortName in {'NITF', 'netCDF'}): + return True + return False
+ + + +
+[docs] +def is_vips(path, matchSize=None): + """ + Check if a path is readable by vips. + + :param path: The path to the file + :param matchSize: if not None, the image read by vips must be the specified + (width, height) tuple in pixels. + :returns: True if readable by vips. + """ + _import_pyvips() + try: + with _newFromFileLock: + image = pyvips.Image.new_from_file(path) + # image(0, 0) will throw if vips can't decode the image + if not image.width or not image.height or image(0, 0) is None: + return False + if matchSize and (matchSize[0] != image.width or matchSize[1] != image.height): + return False + except Exception: + return False + return True
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_converter/format_aperio.html b/_modules/large_image_converter/format_aperio.html new file mode 100644 index 000000000..e7d661702 --- /dev/null +++ b/_modules/large_image_converter/format_aperio.html @@ -0,0 +1,397 @@ + + + + + + large_image_converter.format_aperio — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_converter.format_aperio

+import json
+import math
+
+import large_image_source_tiff
+import tifftools
+
+import large_image
+
+AperioHeader = 'Aperio Image Library v10.0.0\n'
+FullHeaderStart = '{width}x{height} [0,0 {width}x{height}] ({tileSize}x{tileSize})'
+LowHeaderChunk = ' -> {width}x{height}'
+AssociatedHeader = '{name} {width}x{height}'
+ThumbnailHeader = '-> {width}x{height} - |'
+
+
+
+[docs] +def adjust_params(geospatial, params, **kwargs): + """ + Adjust options for aperio format. + + :param geospatial: True if the source is geospatial. + :param params: the conversion options. Possibly modified. + :returns: suffix: the recommended suffix for the new file. + """ + if geospatial: + msg = 'Aperio format cannot be used with geospatial sources.' + raise Exception(msg) + if params.get('subifds') is None: + params['subifds'] = False + return '.svs'
+ + + +
+[docs] +def modify_vips_image_before_output(image, convertParams, **kwargs): + """ + Make sure the vips image is either 1 or 3 bands. + + :param image: a vips image. + :param convertParams: the parameters that will be used for compression. + :returns: a vips image. + """ + return image[:3 if image.bands >= 3 else 1]
+ + + +
+[docs] +def modify_tiled_ifd(info, ifd, idx, ifdIndices, lidata, liDesc, **kwargs): + """ + Modify a tiled image to add aperio metadata and ensure tags are set + appropriately. + + :param info: the tifftools info that will be written to the tiff tile; + modified. + :param ifd: the full resolution ifd as read by tifftools. + :param idx: index of this ifd. + :param ifdIndices: the 0-based index of the full resolution ifd of each + frame followed by the ifd of the first associated image. + :param lidata: large_image data including metadata and associated images. + :param liDesc: the parsed json from the original large_image_converter + description. + """ + descStart = FullHeaderStart.format( + width=ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0], + height=ifd['tags'][tifftools.Tag.ImageHeight.value]['data'][0], + tileSize=ifd['tags'][tifftools.Tag.TileWidth.value]['data'][0], + ) + formatChunk = '-' + if kwargs['compression'] == 'jpeg': + formatChunk = 'JPEG/RGB Q=%s' % (kwargs.get('Q', 90)) + elif kwargs['compression'] == 'jp2k': + formatChunk = 'J2K/YUV16 Q=%s' % (kwargs.get('psnr', kwargs.get('cratios', 0))) + metadata = { + 'Converter': 'large_image_converter', + 'ConverterVersion': liDesc['large_image_converter']['version'], + 'ConverterEpoch': liDesc['large_image_converter']['conversion_epoch'], + 'MPP': (lidata['metadata']['mm_x'] * 1000 + if lidata and lidata['metadata'].get('mm_x') else None), + 'AppMag': (lidata['metadata']['magnification'] + if lidata and lidata['metadata'].get('magnification') else None), + } + compressionTag = ifd['tags'][tifftools.Tag.Compression.value] + if compressionTag['data'][0] == tifftools.constants.Compression.JP2000: + compressionTag['data'][0] = tifftools.constants.Compression.JP2kRGB + if len(ifdIndices) > 2: + metadata['OffsetZ'] = idx + metadata['TotalDepth'] = len(ifdIndices) - 1 + try: + if metadata['IndexRange']['IndexZ'] == metadata['TotalDepth']: + metadata['OffsetZ'] = lidata['metadata']['frames'][idx]['mm_z'] * 1000 + metadata['TotalDepth'] = ( + lidata['metadata']['frames'][-1]['mm_z'] * 2 + + lidata['metadata']['frames'][-2]['mm_z']) + except Exception: + pass + description = ( + f'{AperioHeader}{descStart} {formatChunk}|' + + '|'.join(f'{k} = {v}' for k, v in sorted(metadata.items()) if v is not None)) + ifd['tags'][tifftools.Tag.ImageDescription.value] = { + 'data': description, + 'datatype': tifftools.Datatype.ASCII, + } + ifd['tags'][tifftools.Tag.NewSubfileType.value] = { + 'data': [0], + 'datatype': tifftools.Datatype.LONG, + } + if ifdIndices[idx + 1] == idx + 1: + subifds = ifd['tags'][tifftools.Tag.SubIFD.value]['ifds'] + else: + subifds = info['ifds'][ifdIndices[idx] + 1: ifdIndices[idx + 1]] + for subifd in subifds: + lowerDesc = LowHeaderChunk.format( + width=subifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0], + height=subifd['tags'][tifftools.Tag.ImageHeight.value]['data'][0], + ) + metadata['MPP'] = metadata['MPP'] * 2 if metadata['MPP'] else None + metadata['AppMag'] = metadata['AppMag'] / 2 if metadata['AppMag'] else None + description = ( + f'{AperioHeader}{descStart}{lowerDesc} {formatChunk}|' + + '|'.join(f'{k} = {v}' for k, v in sorted(metadata.items()) if v is not None)) + subifd['tags'][tifftools.Tag.ImageDescription.value] = { + 'data': description, + 'datatype': tifftools.Datatype.ASCII, + } + subifd['tags'][tifftools.Tag.NewSubfileType.value] = { + 'data': [0], + 'datatype': tifftools.Datatype.LONG, + } + compressionTag = subifd['tags'][tifftools.Tag.Compression.value] + if compressionTag['data'][0] == tifftools.constants.Compression.JP2000: + compressionTag['data'][0] = tifftools.constants.Compression.JP2kRGB
+ + + +
+[docs] +def create_thumbnail_and_label(tempPath, info, ifdCount, needsLabel, labelPosition, **kwargs): + """ + Create a thumbnail and, optionally, label image for the aperio file. + + :param tempPath: a temporary file in a temporary directory. + :param info: the tifftools info that will be written to the tiff tile; + modified. + :param ifdCount: the number of ifds in the first tiled image. This is 1 if + there are subifds. + :param needsLabel: true if a label image needs to be added. + :param labelPosition: the position in the ifd list where a label image + should be inserted. + """ + thumbnailSize = 1024 + labelSize = 640 + maxLabelAspect = 1.5 + tileSize = info['ifds'][0]['tags'][tifftools.Tag.TileWidth.value]['data'][0] + levels = int(math.ceil(math.log(max(thumbnailSize, labelSize) / tileSize) / math.log(2))) + 1 + + neededList = ['thumbnail'] + if needsLabel: + neededList[0:0] = ['label'] + tiledPath = tempPath + '-overview.tiff' + firstFrameIfds = info['ifds'][max(0, ifdCount - levels):ifdCount] + tifftools.write_tiff(firstFrameIfds, tiledPath) + ts = large_image_source_tiff.open(tiledPath) + for subImage in neededList: + if subImage == 'label': + x = max(0, (ts.sizeX - min(ts.sizeX, ts.sizeY) * maxLabelAspect) // 2) + y = max(0, (ts.sizeY - min(ts.sizeX, ts.sizeY) * maxLabelAspect) // 2) + regionParams = { + 'output': dict(maxWidth=labelSize, maxHeight=labelSize), + 'region': dict(left=x, right=ts.sizeX - x, top=y, bottom=ts.sizeY - y), + } + else: + regionParams = {'output': dict(maxWidth=thumbnailSize, maxHeight=thumbnailSize)} + image, _ = ts.getRegion( + format=large_image.constants.TILE_FORMAT_PIL, **regionParams) + if image.mode not in {'RGB', 'L'}: + image = image.convert('RGB') + if subImage == 'label': + image = image.rotate(90, expand=True) + imagePath = tempPath + '-image_%s.tiff' % subImage + image.save( + imagePath, 'TIFF', compression='tiff_jpeg', + quality=int(kwargs.get('quality', 90))) + imageInfo = tifftools.read_tiff(imagePath) + ifd = imageInfo['ifds'][0] + if subImage == 'label': + ifd['tags'][tifftools.Tag.Orientation.value] = { + 'data': [tifftools.constants.Orientation.RightTop.value], + 'datatype': tifftools.Datatype.LONG, + } + description = AperioHeader + AssociatedHeader.format( + name='label', + width=ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0], + height=ifd['tags'][tifftools.Tag.ImageHeight.value]['data'][0], + ) + ifd['tags'][tifftools.Tag.ImageDescription.value] = { + 'data': description, + 'datatype': tifftools.Datatype.ASCII, + } + ifd['tags'][tifftools.Tag.NewSubfileType.value] = { + 'data': [ + tifftools.constants.NewSubfileType.ReducedImage.value, + ], + 'datatype': tifftools.Datatype.LONG, + } + info['ifds'][labelPosition:labelPosition] = imageInfo['ifds'] + else: + fullDesc = info['ifds'][0]['tags'][tifftools.Tag.ImageDescription.value]['data'] + description = fullDesc.split('[', 1)[0] + ThumbnailHeader.format( + width=ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0], + height=ifd['tags'][tifftools.Tag.ImageHeight.value]['data'][0], + ) + fullDesc.split('|', 1)[1] + ifd['tags'][tifftools.Tag.ImageDescription.value] = { + 'data': description, + 'datatype': tifftools.Datatype.ASCII, + } + info['ifds'][1:1] = imageInfo['ifds']
+ + + +
+[docs] +def modify_tiff_before_write(info, ifdIndices, tempPath, lidata, **kwargs): + """ + Adjust the metadata and ifds for a tiff file to make it compatible with + Aperio (svs). + + Aperio files are tiff files which are stored without subifds in the order + full res, optional thumbnail, half res, quarter res, ..., full res, half + res, quarter res, ..., label, macro. All ifds have an ImageDescription + that start with an aperio header followed by some dimension information and + then an option key-.value list + + :param info: the tifftools info that will be written to the tiff tile; + modified. + :param ifdIndices: the 0-based index of the full resolution ifd of each + frame followed by the ifd of the first associated image. + :param tempPath: a temporary file in a temporary directory. + :param lidata: large_image data including metadata and associated images. + """ + liDesc = json.loads(info['ifds'][0]['tags'][tifftools.Tag.ImageDescription.value]['data']) + # Adjust tiled images + for idx, ifdIndex in enumerate(ifdIndices[:-1]): + ifd = info['ifds'][ifdIndex] + modify_tiled_ifd(info, ifd, idx, ifdIndices, lidata, liDesc, **kwargs) + # Remove all but macro and label image, keeping track if either is present + assocKeys = set() + for idx in range(len(info['ifds']) - 1, ifdIndices[-1] - 1, -1): + ifd = info['ifds'][idx] + try: + assocKey = ifd['tags'][tifftools.Tag.ImageDescription.value]['data'] + except Exception: + assocKey = 'none' + if assocKey not in {'label', 'macro'}: + info['ifds'][idx: idx + 1] = [] + description = AssociatedHeader.format( + name=assocKey, + width=ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0], + height=ifd['tags'][tifftools.Tag.ImageHeight.value]['data'][0], + ) + ifd['tags'][tifftools.Tag.ImageDescription.value] = { + 'data': description, + 'datatype': tifftools.Datatype.ASCII, + } + ifd['tags'][tifftools.Tag.NewSubfileType.value] = { + 'data': [ + tifftools.constants.NewSubfileType.ReducedImage.value if assocKey == 'label' else + (tifftools.constants.NewSubfileType.ReducedImage.value | + tifftools.constants.NewSubfileType.Macro.value), + ], + 'datatype': tifftools.Datatype.LONG, + } + assocKeys.add(assocKey) + create_thumbnail_and_label( + tempPath, info, ifdIndices[1], 'label' not in assocKeys, ifdIndices[-1], **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_bioformats.html b/_modules/large_image_source_bioformats.html new file mode 100644 index 000000000..85ea0537f --- /dev/null +++ b/_modules/large_image_source_bioformats.html @@ -0,0 +1,886 @@ + + + + + + large_image_source_bioformats — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Source code for large_image_source_bioformats

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+# This tile sources uses javabridge to communicate between python and java.  It
+# requires some version of java's jvm to be available (see
+# https://jdk.java.net/archive/).  It uses the python-bioformats wheel to get
+# the bioformats JAR file.  A later version may be desirable (see
+# https://www.openmicroscopy.org/bio-formats/downloads/).  See
+# https://downloads.openmicroscopy.org/bio-formats/5.1.5/api/loci/formats/
+#   IFormatReader.html for interface details.
+
+import atexit
+import logging
+import math
+import os
+import pathlib
+import re
+import threading
+import types
+import weakref
+import zipfile
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import numpy as np
+
+import large_image.tilesource.base
+from large_image import config
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource, nearPowerOfTwo
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+bioformats = None
+# import javabridge
+javabridge = None
+
+_javabridgeStarted = None
+_javabridgeStartLock = threading.Lock()
+_bioformatsVersion = None
+_openImages = []
+
+
+# Default to ignoring files with no extension and some specific extensions.
+config.ConfigValues['source_bioformats_ignored_names'] = r'(^[^.]*|\.(jpg|jpeg|jpe|png|tif|tiff|ndpi|nd2|ome|nc|json|isyntax|mrxs|zip|zarr(\.db|\.zip)))$'  # noqa
+
+
+def _monitor_thread():
+    main_thread = threading.main_thread()
+    main_thread.join()
+    if len(_openImages):
+        try:
+            javabridge.attach()
+            while len(_openImages):
+                source = _openImages.pop()
+                source = source()
+                try:
+                    source._bioimage.close()
+                except Exception:
+                    pass
+                try:
+                    source._bioimage = None
+                except Exception:
+                    pass
+        except AssertionError:
+            pass
+        finally:
+            if javabridge.get_env():
+                javabridge.detach()
+    _stopJavabridge()
+
+
+def _reduceLogging():
+    # As of python-bioformats 4.0.0, org.apache.log4j isn't in the bundled
+    # jar file, so setting log levels just produces needless warnings.
+    # bioformats.log4j.basic_config()
+    # javabridge.JClassWrapper('loci.common.Log4jTools').setRootLevel(
+    #     logging.getLevelName(logger.level))
+    #
+    # This is taken from
+    # https://github.com/pskeshu/microscoper/blob/master/microscoper/io.py
+    try:
+        rootLoggerName = javabridge.get_static_field(
+            'org/slf4j/Logger', 'ROOT_LOGGER_NAME', 'Ljava/lang/String;')
+        rootLogger = javabridge.static_call(
+            'org/slf4j/LoggerFactory', 'getLogger',
+            '(Ljava/lang/String;)Lorg/slf4j/Logger;', rootLoggerName)
+        logLevel = javabridge.get_static_field(
+            'ch/qos/logback/classic/Level', 'ERROR', 'Lch/qos/logback/classic/Level;')
+        javabridge.call(rootLogger, 'setLevel', '(Lch/qos/logback/classic/Level;)V', logLevel)
+    except Exception:
+        pass
+    bioformats.formatreader.logger.setLevel(logging.ERROR)
+
+
+def _startJavabridge(logger):
+    global _javabridgeStarted, _bioformatsVersion
+
+    with _javabridgeStartLock:
+        if _javabridgeStarted is None:
+            # Only import these when first asked.  They are slow to import.
+            global bioformats
+            global javabridge
+            if bioformats is None:
+                import bioformats
+                try:
+                    _bioformatsVersion = zipfile.ZipFile(
+                        pathlib.Path(bioformats.__file__).parent /
+                        'jars/bioformats_package.jar',
+                    ).open('META-INF/MANIFEST.MF').read(8192).split(
+                        b'Implementation-Version: ')[1].split()[0].decode()
+                    logger.info('Bioformats.jar version: %s', _bioformatsVersion)
+                except Exception:
+                    pass
+            if javabridge is None:
+                import javabridge
+
+            # We need something to wake up at exit and shut things down
+            monitor = threading.Thread(target=_monitor_thread)
+            monitor.daemon = True
+            monitor.start()
+            try:
+                javabridge.start_vm(class_path=bioformats.JARS, run_headless=True)
+                _reduceLogging()
+                atexit.register(_stopJavabridge)
+                logger.info('Started JVM for Bioformats tile source.')
+                _javabridgeStarted = True
+            except RuntimeError:
+                logger.exception('Cannot start JVM for Bioformats tile source.')
+                _javabridgeStarted = False
+    return _javabridgeStarted
+
+
+def _stopJavabridge(*args, **kwargs):
+    global _javabridgeStarted
+
+    if javabridge is not None:
+        javabridge.kill_vm()
+    _javabridgeStarted = None
+
+
+def _getBioformatsVersion():
+    """
+    Get the version of the jar file.
+
+    :returns: the version string if it is in the expected format, None
+        otherwise.
+    """
+    if _bioformatsVersion is None:
+        from large_image import config
+
+        logger = config.getLogger()
+        _startJavabridge(logger)
+    return _bioformatsVersion
+
+
+
+[docs] +class BioformatsFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to via Bioformats. + """ + + cacheName = 'tilesource' + name = 'bioformats' + extensions = { + None: SourcePriority.FALLBACK, + 'czi': SourcePriority.PREFERRED, + 'ets': SourcePriority.LOW, # part of vsi + 'lif': SourcePriority.MEDIUM, + 'vsi': SourcePriority.PREFERRED, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'image/czi': SourcePriority.PREFERRED, + 'image/vsi': SourcePriority.PREFERRED, + } + + # If frames are smaller than this they are served as single tiles, which + # can be more efficient than handling multiple tiles. + _singleTileThreshold = 2048 + _tileSize = 512 + _associatedImageMaxSize = 8192 + _maxSkippedLevels = 3 + + def __init__(self, path, **kwargs): # noqa + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: the associated file path. + """ + super().__init__(path, **kwargs) + + largeImagePath = str(self._getLargeImagePath()) + config._ignoreSourceNames('bioformats', largeImagePath) + + if not _startJavabridge(self.logger): + msg = 'File cannot be opened by bioformats reader because javabridge failed to start' + raise TileSourceError(msg) + self.addKnownExtensions() + + self._tileLock = threading.RLock() + + try: + javabridge.attach() + try: + self._bioimage = bioformats.ImageReader(largeImagePath, perform_init=False) + try: + # So this as a separate step so, if it fails, we can ask to + # open something that does not exist and bioformats will + # release some file handles. + self._bioimage.init_reader() + except Exception as exc: + try: + # Ask to open a file that should never exist + self._bioimage.rdr.setId('__\0__') + except Exception: + pass + self._bioimage.close() + self._bioimage = None + raise exc + except (AttributeError, OSError) as exc: + if not os.path.isfile(largeImagePath): + raise TileSourceFileNotFoundError(largeImagePath) from None + self.logger.debug('File cannot be opened via Bioformats. (%r)', exc) + raise TileSourceError('File cannot be opened via Bioformats (%r)' % exc) + _openImages.append(weakref.ref(self)) + + rdr = self._bioimage.rdr + # Bind additional functions not done by bioformats module. + # Functions are listed at https://downloads.openmicroscopy.org + # /bio-formats/5.1.5/api/loci/formats/IFormatReader.html + for (name, params, desc) in [ + ('getBitsPerPixel', '()I', 'Get the number of bits per pixel'), + ('getDomains', '()[Ljava/lang/String;', 'Get a list of domains'), + ('getEffectiveSizeC', '()I', 'effectiveC * Z * T = imageCount'), + ('getOptimalTileHeight', '()I', 'the optimal sub-image height ' + 'for use with openBytes'), + ('getOptimalTileWidth', '()I', 'the optimal sub-image width ' + 'for use with openBytes'), + ('getResolution', '()I', 'The current resolution level'), + ('getResolutionCount', '()I', 'The number of resolutions for ' + 'the current series'), + ('getZCTCoords', '(I)[I', 'Gets the Z, C and T coordinates ' + '(real sizes) corresponding to the given rasterized index value.'), + ('hasFlattenedResolutions', '()Z', 'True if resolutions have been flattened'), + ('isMetadataComplete', '()Z', 'True if metadata is completely parsed'), + ('isNormalized', '()Z', 'Is float data normalized'), + ('setFlattenedResolutions', '(Z)V', 'Set if resolution should be flattened'), + ('setResolution', '(I)V', 'Set the resolution level'), + ]: + setattr(rdr, name, types.MethodType( + javabridge.jutil.make_method(name, params, desc), rdr)) + # rdr.setFlattenedResolutions(False) + self._metadataForCurrentSeries(rdr) + self._checkSeries(rdr) + bmd = bioformats.metadatatools.MetadataRetrieve(self._bioimage.metadata) + try: + self._metadata['channelNames'] = [ + bmd.getChannelName(0, c) or bmd.getChannelID(0, c) + for c in range(self._metadata['sizeColorPlanes'])] + except Exception: + self._metadata['channelNames'] = [] + for key in ['sizeXY', 'sizeC', 'sizeZ', 'sizeT']: + if not isinstance(self._metadata[key], int) or self._metadata[key] < 1: + self._metadata[key] = 1 + self.sizeX = self._metadata['sizeX'] + self.sizeY = self._metadata['sizeY'] + if self.sizeX <= 0 or self.sizeY <= 0: + msg = 'File cannot be opened with biofromats.' + raise TileSourceError(msg) + self._computeTiles() + self._computeLevels() + self._computeMagnification() + except javabridge.JavaException as exc: + es = javabridge.to_string(exc.throwable) + self.logger.debug('File cannot be opened via Bioformats. (%s)', es) + raise TileSourceError('File cannot be opened via Bioformats. (%s)' % es) + except (AttributeError, UnicodeDecodeError): + self.logger.exception('The bioformats reader threw an unhandled exception.') + msg = 'The bioformats reader threw an unhandled exception.' + raise TileSourceError(msg) + finally: + if javabridge.get_env(): + javabridge.detach() + + if self.levels < 1: + msg = 'Bioformats image must have at least one level.' + raise TileSourceError(msg) + + if self.sizeX <= 0 or self.sizeY <= 0: + msg = 'Bioformats tile size is invalid.' + raise TileSourceError(msg) + try: + self.getTile(0, 0, self.levels - 1) + except Exception as exc: + raise TileSourceError('Bioformats cannot read a tile: %r' % exc) + self._populatedLevels = len([ + v for v in self._metadata['frameSeries'][0]['series'] if v is not None]) + + def __del__(self): + if getattr(self, '_bioimage', None) is not None: + try: + javabridge.attach() + self._bioimage.close() + del self._bioimage + _openImages.remove(weakref.ref(self)) + except Exception: + pass + finally: + if javabridge.get_env(): + javabridge.detach() + + def _metadataForCurrentSeries(self, rdr): + self._metadata = getattr(self, '_metadata', {}) + self._metadata.update({ + 'dimensionOrder': rdr.getDimensionOrder(), + 'metadata': javabridge.jdictionary_to_string_dictionary( + rdr.getMetadata()), + 'seriesMetadata': javabridge.jdictionary_to_string_dictionary( + rdr.getSeriesMetadata()), + 'seriesCount': rdr.getSeriesCount(), + 'imageCount': rdr.getImageCount(), + 'rgbChannelCount': rdr.getRGBChannelCount(), + 'sizeColorPlanes': rdr.getSizeC(), + 'sizeT': rdr.getSizeT(), + 'sizeZ': rdr.getSizeZ(), + 'sizeX': rdr.getSizeX(), + 'sizeY': rdr.getSizeY(), + 'pixelType': rdr.getPixelType(), + 'isLittleEndian': rdr.isLittleEndian(), + 'isRGB': rdr.isRGB(), + 'isInterleaved': rdr.isInterleaved(), + 'isIndexed': rdr.isIndexed(), + 'bitsPerPixel': rdr.getBitsPerPixel(), + 'sizeC': rdr.getEffectiveSizeC(), + 'normalized': rdr.isNormalized(), + 'metadataComplete': rdr.isMetadataComplete(), + # 'domains': rdr.getDomains(), + 'optimalTileWidth': rdr.getOptimalTileWidth(), + 'optimalTileHeight': rdr.getOptimalTileHeight(), + 'resolutionCount': rdr.getResolutionCount(), + }) + + def _getSeriesStarts(self, rdr): # noqa + self._metadata['frameSeries'] = [{ + 'series': [0], + 'sizeX': self._metadata['sizeX'], + 'sizeY': self._metadata['sizeY'], + }] + if self._metadata['seriesCount'] <= 1: + return 1 + seriesMetadata = {} + for idx in range(self._metadata['seriesCount']): + rdr.setSeries(idx) + seriesMetadata.update( + javabridge.jdictionary_to_string_dictionary(rdr.getSeriesMetadata())) + frameList = [] + nextSeriesNum = 0 + try: + for key, value in seriesMetadata.items(): + frameNum = int(value) + seriesNum = int(key.split('Series ')[1].split('|')[0]) - 1 + if seriesNum >= 0 and seriesNum < self._metadata['seriesCount']: + while len(frameList) <= frameNum: + frameList.append([]) + if seriesNum not in frameList[frameNum]: + frameList[frameNum].append(seriesNum) + frameList[frameNum].sort() + nextSeriesNum = max(nextSeriesNum, seriesNum + 1) + except Exception as exc: + self.logger.debug('Failed to parse series information: %s', exc) + rdr.setSeries(0) + if any(key for key in seriesMetadata if key.startswith('Series ')): + return 1 + if not len(seriesMetadata) or not any( + key for key in seriesMetadata if key.startswith('Series ')): + frameList = [[0]] + nextSeriesNum = 1 + rdr.setSeries(0) + lastX, lastY = rdr.getSizeX(), rdr.getSizeY() + for idx in range(1, self._metadata['seriesCount']): + rdr.setSeries(idx) + if (rdr.getSizeX() == self._metadata['sizeX'] and + rdr.getSizeY() == self._metadata['sizeY'] and + rdr.getImageCount() == self._metadata['imageCount']): + frameList.append([idx]) + if nextSeriesNum == idx: + nextSeriesNum = idx + 1 + lastX, lastY = self._metadata['sizeX'], self._metadata['sizeY'] + if (rdr.getSizeX() * rdr.getSizeY() * rdr.getImageCount() > + self._metadata['sizeX'] * self._metadata['sizeY'] * + self._metadata['imageCount']): + frameList = [[idx]] + nextSeriesNum = idx + 1 + self._metadata['sizeX'] = self.sizeX = lastX = rdr.getSizeX() + self._metadata['sizeY'] = self.sizeY = lastY = rdr.getSizeY() + self._metadata['imageCount'] = rdr.getImageCount() + if (lastX and lastY and + nearPowerOfTwo(rdr.getSizeX(), lastX) and rdr.getSizeX() < lastX and + nearPowerOfTwo(rdr.getSizeY(), lastY) and rdr.getSizeY() < lastY): + steps = int(round(math.log( + lastX * lastY / (rdr.getSizeX() * rdr.getSizeY())) / math.log(2) / 2)) + frameList[-1] += [None] * (steps - 1) + frameList[-1].append(idx) + lastX, lastY = rdr.getSizeX(), rdr.getSizeY() + if nextSeriesNum == idx: + nextSeriesNum = idx + 1 + frameList = [fl for fl in frameList if len(fl)] + self._metadata['frameSeries'] = [{ + 'series': fl, + } for fl in frameList] + rdr.setSeries(0) + return nextSeriesNum + + def _checkSeries(self, rdr): + firstPossibleAssoc = self._getSeriesStarts(rdr) + self._metadata['seriesAssociatedImages'] = {} + for seriesNum in range(firstPossibleAssoc, self._metadata['seriesCount']): + if any((seriesNum in series['series']) for series in self._metadata['frameSeries']): + continue + rdr.setSeries(seriesNum) + info = { + 'sizeX': rdr.getSizeX(), + 'sizeY': rdr.getSizeY(), + } + if (info['sizeX'] < self._associatedImageMaxSize and + info['sizeY'] < self._associatedImageMaxSize): + # TODO: Figure out better names for associated images. Can + # we tell if any of them are the macro or label image? + info['seriesNum'] = seriesNum + self._metadata['seriesAssociatedImages'][ + 'image%d' % seriesNum] = info + validate = None + for frame in self._metadata['frameSeries']: + for level in range(len(frame['series'])): + if level and frame['series'][level] is None: + continue + rdr.setSeries(frame['series'][level]) + self._metadataForCurrentSeries(rdr) + info = { + 'sizeX': rdr.getSizeX(), + 'sizeY': rdr.getSizeY(), + } + if not level: + frame.update(info) + self._metadata['sizeX'] = max(self._metadata['sizeX'], frame['sizeX']) + self._metadata['sizeY'] = max(self._metadata['sizeY'], frame['sizeY']) + elif validate is not False: + if (not nearPowerOfTwo(frame['sizeX'], info['sizeX']) or + not nearPowerOfTwo(frame['sizeY'], info['sizeY'])): + frame['series'] = frame['series'][:level] + validate = True + break + rdr.setSeries(frame['series'][0]) + self._metadataForCurrentSeries(rdr) + if validate is None: + validate = False + rdr.setSeries(0) + self._metadata['sizeXY'] = len(self._metadata['frameSeries']) + + def _computeTiles(self): + if (self._metadata['resolutionCount'] <= 1 and + self.sizeX <= self._singleTileThreshold and + self.sizeY <= self._singleTileThreshold): + self.tileWidth = self.sizeX + self.tileHeight = self.sizeY + elif (128 <= self._metadata['optimalTileWidth'] <= self._singleTileThreshold and + 128 <= self._metadata['optimalTileHeight'] <= self._singleTileThreshold): + self.tileWidth = self._metadata['optimalTileWidth'] + self.tileHeight = self._metadata['optimalTileHeight'] + else: + self.tileWidth = self.tileHeight = self._tileSize + + def _computeLevels(self): + self.levels = int(math.ceil(max( + math.log(float(self.sizeX) / self.tileWidth), + math.log(float(self.sizeY) / self.tileHeight)) / math.log(2))) + 1 + + def _computeMagnification(self): + self._magnification = {} + metadata = self._metadata['metadata'] + valuekeys = { + 'x': [('Scaling|Distance|Value #1', 1e3)], + 'y': [('Scaling|Distance|Value #2', 1e3)], + } + tuplekeys = [ + ('Physical pixel size', 1e-3), + ] + magkeys = [ + 'Information|Instrument|Objective|NominalMagnification #1', + 'Magnification #1', + ] + for axis in {'x', 'y'}: + for key, units in valuekeys[axis]: + if metadata.get(key): + self._magnification['mm_' + axis] = float(metadata[key]) * units + if 'mm_x' not in self._magnification and 'mm_y' not in self._magnification: + for key, units in tuplekeys: + if metadata.get(key): + found = re.match(r'^\D*(\d+(|\.\d+))\D+(\d+(|\.\d+))\D*$', metadata[key]) + if found: + try: + self._magnification['mm_x'], self._magnification['mm_y'] = ( + float(found.groups()[0]) * units, float(found.groups()[2]) * units) + except Exception: + pass + for key in magkeys: + if metadata.get(key): + self._magnification['magnification'] = float(metadata[key]) + break + + def _nonemptyLevelsList(self, frame=0): + """ + Return a list of one value per level where the value is None if the + level does not exist in the file and any other value if it does. + + :param frame: the frame number. + :returns: a list of levels length. + """ + nonempty = [True if v is not None else None + for v in self._metadata['frameSeries'][0]['series']][:self.levels] + nonempty += [None] * (self.levels - len(nonempty)) + return nonempty[::-1] + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + mm_x = self._magnification.get('mm_x') + mm_y = self._magnification.get('mm_y', mm_x) + # Estimate the magnification if we don't have a direct value + mag = self._magnification.get('magnification') or 0.01 / mm_x if mm_x else None + return { + 'magnification': mag, + 'mm_x': mm_x, + 'mm_y': mm_y, + }
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + + """ + result = super().getMetadata() + # sizeC, sizeZ, sizeT, sizeXY + frames = [] + for xy in range(self._metadata['sizeXY']): + for t in range(self._metadata['sizeT']): + for z in range(self._metadata['sizeZ']): + for c in range(self._metadata['sizeC']): + frames.append({ + 'IndexC': c, + 'IndexZ': z, + 'IndexT': t, + 'IndexXY': xy, + }) + if len(self._metadata['frameSeries']) == len(frames): + for idx, frame in enumerate(frames): + frame['sizeX'] = self._metadata['frameSeries'][idx]['sizeX'] + frame['sizeY'] = self._metadata['frameSeries'][idx]['sizeY'] + frame['levels'] = len(self._metadata['frameSeries'][idx]['series']) + if len(frames) > 1: + result['frames'] = frames + self._addMetadataFrameInformation(result, self._metadata['channelNames']) + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + return self._metadata
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + self._xyzInRange(x, y, z) + ft = fc = fz = 0 + fseries = self._metadata['frameSeries'][0] + if kwargs.get('frame') is not None: + frame = self._getFrame(**kwargs) + fc = frame % self._metadata['sizeC'] + fz = (frame // self._metadata['sizeC']) % self._metadata['sizeZ'] + ft = (frame // self._metadata['sizeC'] // + self._metadata['sizeZ']) % self._metadata['sizeT'] + fxy = (frame // self._metadata['sizeC'] // + self._metadata['sizeZ'] // self._metadata['sizeT']) + if frame < 0 or fxy > self._metadata['sizeXY']: + msg = 'Frame does not exist' + raise TileSourceError(msg) + fseries = self._metadata['frameSeries'][fxy] + seriesLevel = self.levels - 1 - z + scale = 1 + while seriesLevel >= len(fseries['series']) or fseries['series'][seriesLevel] is None: + seriesLevel -= 1 + scale *= 2 + offsetx = x * self.tileWidth * scale + offsety = y * self.tileHeight * scale + width = min(self.tileWidth * scale, self.sizeX // 2 ** seriesLevel - offsetx) + height = min(self.tileHeight * scale, self.sizeY // 2 ** seriesLevel - offsety) + sizeXAtScale = fseries['sizeX'] // (2 ** seriesLevel) + sizeYAtScale = fseries['sizeY'] // (2 ** seriesLevel) + finalWidth = width // scale + finalHeight = height // scale + width = min(width, sizeXAtScale - offsetx) + height = min(height, sizeYAtScale - offsety) + + if scale >= 2 ** self._maxSkippedLevels: + tile = self._getTileFromEmptyLevel(x, y, z, **kwargs) + tile = large_image.tilesource.base._imageToNumpy(tile)[0] + format = TILE_FORMAT_NUMPY + else: + with self._tileLock: + try: + javabridge.attach() + if width > 0 and height > 0: + tile = self._bioimage.read( + c=fc, z=fz, t=ft, series=fseries['series'][seriesLevel], + rescale=False, # return internal data types + XYWH=(offsetx, offsety, width, height)) + else: + # We need the same dtype, so read 1x1 at 0x0 + tile = self._bioimage.read( + c=fc, z=fz, t=ft, series=fseries['series'][seriesLevel], + rescale=False, # return internal data types + XYWH=(0, 0, 1, 1)) + tile = np.zeros(tuple([0, 0] + list(tile.shape[2:])), dtype=tile.dtype) + format = TILE_FORMAT_NUMPY + except javabridge.JavaException as exc: + es = javabridge.to_string(exc.throwable) + raise TileSourceError('Failed to get Bioformat region (%s, %r).' % (es, ( + fc, fz, ft, fseries, self.sizeX, self.sizeY, offsetx, + offsety, width, height))) + finally: + if javabridge.get_env(): + javabridge.detach() + if scale > 1: + tile = tile[::scale, ::scale] + if tile.shape[:2] != (finalHeight, finalWidth): + fillValue = 0 + if tile.dtype == np.uint16: + fillValue = 65535 + elif tile.dtype == np.uint8: + fillValue = 255 + elif tile.dtype.kind == 'f': + fillValue = 1 + retile = np.full( + tuple([finalHeight, finalWidth] + list(tile.shape[2:])), + fillValue, + dtype=tile.dtype) + retile[0:min(tile.shape[0], finalHeight), 0:min(tile.shape[1], finalWidth)] = tile[ + 0:min(tile.shape[0], finalHeight), 0:min(tile.shape[1], finalWidth)] + tile = retile + return self._outputTile(tile, format, x, y, z, pilImageAllowed, numpyAllowed, **kwargs)
+ + +
+[docs] + def getAssociatedImagesList(self): + """ + Return a list of associated images. + + :return: the list of image keys. + """ + return sorted(self._metadata['seriesAssociatedImages'].keys())
+ + + def _getAssociatedImage(self, imageKey): + """ + Get an associated image in PIL format. + + :param imageKey: the key of the associated image. + :return: the image in PIL format or None. + """ + info = self._metadata['seriesAssociatedImages'].get(imageKey) + if info is None: + return + series = info['seriesNum'] + with self._tileLock: + try: + javabridge.attach() + image = self._bioimage.read( + series=series, + rescale=False, # return internal data types + XYWH=(0, 0, info['sizeX'], info['sizeY'])) + except javabridge.JavaException as exc: + es = javabridge.to_string(exc.throwable) + raise TileSourceError('Failed to get Bioformat series (%s, %r).' % (es, ( + series, info['sizeX'], info['sizeY']))) + finally: + if javabridge.get_env(): + javabridge.detach() + return large_image.tilesource.base._imageToPIL(image) + +
+[docs] + @classmethod + def addKnownExtensions(cls): + # This starts javabridge/bioformats if needed + _getBioformatsVersion() + if not hasattr(cls, '_addedExtensions'): + cls._addedExtensions = True + cls.extensions = cls.extensions.copy() + for dotext in bioformats.READABLE_FORMATS: + ext = dotext.strip('.') + if ext not in cls.extensions: + cls.extensions[ext] = SourcePriority.IMPLICIT
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return BioformatsFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return BioformatsFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_bioformats/girder_source.html b/_modules/large_image_source_bioformats/girder_source.html new file mode 100644 index 000000000..3c00b2f44 --- /dev/null +++ b/_modules/large_image_source_bioformats/girder_source.html @@ -0,0 +1,161 @@ + + + + + + large_image_source_bioformats.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_bioformats.girder_source

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import cherrypy
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import BioformatsFileTileSource, _stopJavabridge
+
+cherrypy.engine.subscribe('stop', _stopJavabridge)
+
+
+
+[docs] +class BioformatsGirderTileSource(BioformatsFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items that can be read with bioformats. + """ + + cacheName = 'tilesource' + name = 'bioformats' + +
+[docs] + def mayHaveAdjacentFiles(self, largeImageFile): + # bioformats uses extensions to determine how to open a file, so this + # needs to be set for all file formats. + return True
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_deepzoom.html b/_modules/large_image_source_deepzoom.html new file mode 100644 index 000000000..83235568e --- /dev/null +++ b/_modules/large_image_source_deepzoom.html @@ -0,0 +1,261 @@ + + + + + + large_image_source_deepzoom — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_deepzoom

+import builtins
+import math
+import os
+from xml.etree import ElementTree
+
+import PIL.Image
+
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_PIL, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource, etreeToDict
+
+
+
+[docs] +class DeepzoomFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to a Deepzoom xml (dzi) file and associated pngs/jpegs + in relative folders on the local file system. + """ + + cacheName = 'tilesource' + name = 'deepzoom' + extensions = { + None: SourcePriority.LOW, + 'dzi': SourcePriority.HIGH, + 'dzc': SourcePriority.HIGH, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + } + + def __init__(self, path, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + self._largeImagePath = self._getLargeImagePath() + # Read the root dzi file and check that the expected image files exist + try: + with builtins.open(self._largeImagePath) as fptr: + if fptr.read(1024).strip()[:5] != '<?xml': + msg = 'File cannot be opened via deepzoom reader.' + raise TileSourceError(msg) + fptr.seek(0) + xml = ElementTree.parse(self._largeImagePath).getroot() + self._info = etreeToDict(xml)['Image'] + except (ElementTree.ParseError, KeyError, UnicodeDecodeError): + msg = 'File cannot be opened via deepzoom reader.' + raise TileSourceError(msg) + except FileNotFoundError: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + raise + # We should now have a dictionary like + # {'Format': 'png', # or 'jpeg' + # 'Overlap': '1', + # 'Size': {'Height': '41784', 'Width': '44998'}, + # 'TileSize': '254'} + # and a file structure like + # <rootname>_files/<level>/<x>_<y>.<format> + # images will be TileSize+Overlap square; final images will be + # truncated. Base level is either 0 or probably 8 (level 0 is a 1x1 + # pixel tile) + self.sizeX = int(self._info['Size']['Width']) + self.sizeY = int(self._info['Size']['Height']) + self.tileWidth = self.tileHeight = int(self._info['TileSize']) + maxXY = max(self.sizeX, self.sizeY) + self.levels = int(math.ceil( + math.log(maxXY / self.tileWidth) / math.log(2))) + 1 + tiledirName = os.path.splitext(os.path.basename(self._largeImagePath))[0] + '_files' + rootdir = os.path.dirname(self._largeImagePath) + self._tiledir = os.path.join(rootdir, tiledirName) + if not os.path.isdir(self._tiledir): + rootdir = os.path.dirname(rootdir) + self._tiledir = os.path.join(rootdir, tiledirName) + zeroname = '0_0.%s' % self._info['Format'] + self._nested = os.path.isdir(os.path.join(self._tiledir, '0', zeroname)) + zeroimg = PIL.Image.open( + os.path.join(self._tiledir, '0', zeroname) if not self._nested else + os.path.join(self._tiledir, '0', zeroname, zeroname)) + if zeroimg.size == (1, 1): + self._baselevel = int( + math.ceil(math.log(maxXY) / math.log(2)) - + math.ceil(math.log(maxXY / self.tileWidth) / math.log(2))) + else: + self._baselevel = 0 + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + result = {} + result['deepzoom'] = self._info + result['baselevel'] = self._baselevel + return result
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + self._xyzInRange(x, y, z) + tilename = '%d_%d.%s' % (x, y, self._info['Format']) + tilepath = os.path.join(self._tiledir, '%d' % (self._baselevel + z), tilename) + if self._nested: + tilepath = os.path.join(tilepath, tilename) + tile = PIL.Image.open(tilepath) + overlap = int(self._info.get('Overlap', 0)) + tile = tile.crop(( + overlap if x else 0, overlap if y else 0, + self.tileWidth + (overlap if x else 0), + self.tileHeight + (overlap if y else 0))) + return self._outputTile(tile, TILE_FORMAT_PIL, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+
+ + + +
+[docs] +def open(*args, **kwargs): + """Create an instance of the module class.""" + return DeepzoomFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """Check if an input can be read by the module class.""" + return DeepzoomFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_deepzoom/girder_source.html b/_modules/large_image_source_deepzoom/girder_source.html new file mode 100644 index 000000000..dab3bde83 --- /dev/null +++ b/_modules/large_image_source_deepzoom/girder_source.html @@ -0,0 +1,140 @@ + + + + + + large_image_source_deepzoom.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_deepzoom.girder_source

+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import DeepzoomFileTileSource
+
+
+
+[docs] +class DeepzoomGirderTileSource(DeepzoomFileTileSource, GirderTileSource): + """ + Deepzoom large_image tile source for Girder. + + Provides tile access to Girder items with a Deepzoom xml (dzi) file and + associated pngs/jpegs in relative folders and items or on the local file + system. + """ + + cacheName = 'tilesource' + name = 'deepzoom' + + _mayHaveAdjacentFiles = True
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom.html b/_modules/large_image_source_dicom.html new file mode 100644 index 000000000..cc54c5a1b --- /dev/null +++ b/_modules/large_image_source_dicom.html @@ -0,0 +1,572 @@ + + + + + + large_image_source_dicom — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom

+import math
+import os
+import re
+import warnings
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import numpy as np
+from large_image_source_dicom.dicom_metadata import extract_dicom_metadata
+from large_image_source_dicom.dicomweb_utils import get_dicomweb_metadata
+
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_PIL, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource
+from large_image.tilesource.utilities import _imageToNumpy, _imageToPIL
+
+dicomweb_client = None
+pydicom = None
+wsidicom = None
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+def _lazyImport():
+    """
+    Import the wsidicom module.  This is done when needed rather than in the
+    module initialization because it is slow.
+    """
+    global wsidicom
+    global dicomweb_client
+    global pydicom
+
+    if wsidicom is None:
+        try:
+            import dicomweb_client
+            import pydicom
+            import wsidicom
+        except ImportError:
+            msg = 'dicom modules not found.'
+            raise TileSourceError(msg)
+        warnings.filterwarnings('ignore', category=UserWarning, module='wsidicom')
+        warnings.filterwarnings('ignore', category=UserWarning, module='dicomweb_client')
+        warnings.filterwarnings('ignore', category=UserWarning, module='pydicom')
+
+
+def _lazyImportPydicom():
+    global pydicom
+
+    if pydicom is None:
+        import pydicom
+    return pydicom
+
+
+
+[docs] +def dicom_to_dict(ds, base=None): + """ + Convert a pydicom dataset to a fairly flat python dictionary for purposes + of reporting. This is not invertable without extra work. + + :param ds: a pydicom dataset. + :param base: a base dataset entry within the dataset. + :returns: a dictionary of values. + """ + if base is None: + base = ds.to_json_dict( + bulk_data_threshold=0, + bulk_data_element_handler=lambda x: '<%s bytes>' % len(x.value)) + info = {} + for k, v in base.items(): + key = k + try: + key = pydicom.datadict.keyword_for_tag(k) + except Exception: + pass + if isinstance(v, str): + val = v + else: + if v.get('vr') in {None, 'OB'}: + continue + if not len(v.get('Value', [])): + continue + if isinstance(v['Value'][0], dict): + val = [dicom_to_dict(ds, entry) for entry in v['Value']] + elif len(v['Value']) == 1: + val = v['Value'][0] + else: + val = v['Value'] + info[key] = val + return info
+ + + +
+[docs] +class DICOMFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to dicom files the dicom or dicomreader library can read. + """ + + cacheName = 'tilesource' + name = 'dicom' + extensions = { + None: SourcePriority.LOW, + 'dcm': SourcePriority.PREFERRED, + 'dic': SourcePriority.PREFERRED, + 'dicom': SourcePriority.PREFERRED, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'application/dicom': SourcePriority.PREFERRED, + } + nameMatches = { + r'DCM_\d+$': SourcePriority.MEDIUM, + r'\d+(\.\d+){3,20}$': SourcePriority.MEDIUM, + } + + _minTileSize = 64 + _maxTileSize = 4096 + + def __init__(self, path, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + self._dicomWebClient = None + + # We want to make a list of paths of files in this item, if multiple, + # or adjacent items in the folder if the item is a single file. We + # filter files with names that have a preferred extension. + # If the path is a dict, that likely means it is a DICOMweb asset. + path = self._getLargeImagePath() + if not isinstance(path, (dict, list)): + path = str(path) + if not os.path.isfile(path): + raise TileSourceFileNotFoundError(path) from None + root = os.path.dirname(path) or '.' + try: + _lazyImportPydicom() + pydicom.filereader.dcmread(path, stop_before_pixels=True) + except Exception as exc: + msg = f'File cannot be opened via dicom tile source ({exc}).' + raise TileSourceError(msg) + self._largeImagePath = [ + os.path.join(root, entry) for entry in os.listdir(root) + if os.path.isfile(os.path.join(root, entry)) and + self._pathMightBeDicom(os.path.join(root, entry), path)] + if (path not in self._largeImagePath and + os.path.join(root, os.path.basename(path)) not in self._largeImagePath): + self._largeImagePath = [path] + else: + self._largeImagePath = path + _lazyImport() + try: + self._dicom = self._open_wsi_dicom(self._largeImagePath) + # To let Python 3.8 work -- if this is insufficient, we may have to + # drop 3.8 support. + if not hasattr(self._dicom, 'pyramids'): + self._dicom.pyramids = [self._dicom.levels] + except Exception as exc: + msg = f'File cannot be opened via dicom tile source ({exc}).' + raise TileSourceError(msg) + + self.sizeX = int(self._dicom.size.width) + self.sizeY = int(self._dicom.size.height) + self.tileWidth = int(self._dicom.tile_size.width) + self.tileHeight = int(self._dicom.tile_size.height) + self.tileWidth = min(max(self.tileWidth, self._minTileSize), self._maxTileSize) + self.tileHeight = min(max(self.tileHeight, self._minTileSize), self._maxTileSize) + self.levels = int(max(1, math.ceil(math.log( + max(self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1)) + self._populatedLevels = len(self._dicom.pyramids[0]) + # We need to detect which levels are functionally present if we want to + # return a sensible _nonemptyLevelsList + + @property + def _isDicomWeb(self): + # Keep track of whether this is DICOMweb or not + return self._dicomWebClient is not None + + def _open_wsi_dicom(self, path): + if isinstance(path, dict): + # Use the DICOMweb open method + return self._open_wsi_dicomweb(path) + else: + # Use the regular open method + return wsidicom.WsiDicom.open(path) + + def _open_wsi_dicomweb(self, info): + # These are the required keys in the info dict + url = info['url'] + study_uid = info['study_uid'] + series_uid = info['series_uid'] + + # Create the web client + client = dicomweb_client.DICOMwebClient( + url, + # The following are optional keys + qido_url_prefix=info.get('qido_prefix'), + wado_url_prefix=info.get('wado_prefix'), + session=info.get('session'), + ) + + wsidicom_client = wsidicom.WsiDicomWebClient(client) + + # Save this for future use + self._dicomWebClient = client + + # Open the WSI DICOMweb file + return wsidicom.WsiDicom.open_web(wsidicom_client, study_uid, series_uid) + + def __del__(self): + # If we have an _unstyledInstance attribute, this is not the owner of + # the _docim handle, so we can't close it. Otherwise, we need to close + # it or the _dicom library may prevent shutting down. + if getattr(self, '_dicom', None) is not None and not hasattr(self, '_derivedSource'): + try: + self._dicom.close() + finally: + self._dicom = None + + def _pathMightBeDicom(self, path, basepath=None): + """ + Return True if the path looks like it might be a dicom file based on + its name or extension. + + :param path: the path to check. + :returns: True if this might be a dicom, False otherwise. + """ + mightbe = False + origpath = path + path = os.path.basename(path) + if (basepath is not None and + os.path.splitext(os.path.basename(basepath))[-1][1:] not in self.extensions): + if os.path.splitext(path)[-1] == os.path.splitext(os.path.basename(basepath))[-1]: + mightbe = True + elif os.path.splitext(path)[-1][1:] in self.extensions: + + mightbe = True + if (not mightbe and re.match(r'^([1-9][0-9]*|0)(\.([1-9][0-9]*|0))+$', path) and + len(path) <= 64): + mightbe = True + if not mightbe and re.match(r'^DCM_\d+$', path): + mightbe = True + if mightbe and basepath: + try: + _lazyImportPydicom() + base = pydicom.filereader.dcmread(basepath, stop_before_pixels=True) + except Exception as exc: + msg = f'File cannot be opened via dicom tile source ({exc}).' + raise TileSourceError(msg) + try: + base_series_uid = base[pydicom.tag.Tag('SeriesInstanceUID')].value + except Exception: + base_series_uid = None + if base_series_uid: + try: + series_uid = pydicom.filereader.dcmread( + origpath, stop_before_pixels=True, + )[pydicom.tag.Tag('SeriesInstanceUID')].value + mightbe = base_series_uid == series_uid + except Exception: + mightbe = False + return mightbe + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + mm_x = mm_y = None + try: + mm_x = self._dicom.pyramids[0][0].pixel_spacing.width or None + mm_y = self._dicom.pyramids[0][0].pixel_spacing.height or None + except Exception: + pass + mm_x = float(mm_x) if mm_x else None + mm_y = float(mm_y) if mm_y else None + # Estimate the magnification; we don't have a direct value + mag = 0.01 / mm_x if mm_x else None + return { + 'magnification': mag, + 'mm_x': mm_x, + 'mm_y': mm_y, + }
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + result = super().getMetadata() + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + result = {} + idx = 0 + for level in self._dicom.pyramids[0]: + for ds in level.datasets: + result.setdefault('dicom', {}) + info = dicom_to_dict(ds) + if not idx: + result['dicom'] = info + else: + for k, v in info.items(): + if k not in result['dicom'] or v != result['dicom'][k]: + result['dicom']['%s:%d' % (k, idx)] = v + idx += 1 + + result['dicom_meta'] = self._getDicomMetadata() + + return result
+ + + @methodcache() + def _getDicomMetadata(self): + if self._isDicomWeb: + # Get the client, study uid, and series uid + client = self._dicomWebClient + study_uid = self._dicom.uids.study_instance + series_uid = self._dicom.uids.series_instance + return get_dicomweb_metadata(client, study_uid, series_uid) + else: + # Find the first volume instance and extract the metadata + volume = None + for level in self._dicom.pyramids[0]: + for ds in level.datasets: + if ds.image_type.value == 'VOLUME': + volume = ds + break + + if volume: + break + + if not volume: + return None + + return extract_dicom_metadata(volume) + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame) + x0, y0, x1, y1, step = self._xyzToCorners(x, y, z) + bw = self.tileWidth * step + bh = self.tileHeight * step + level = 0 + levelfactor = 1 + basefactor = self._dicom.pyramids[0][0].pixel_spacing.width + for checklevel in range(1, len(self._dicom.pyramids[0])): + factor = round(self._dicom.pyramids[0][checklevel].pixel_spacing.width / basefactor) + if factor <= step: + level = checklevel + levelfactor = factor + else: + break + x0f = int(x0 // levelfactor) + y0f = int(y0 // levelfactor) + x1f = min(int(math.ceil(x1 / levelfactor)), self._dicom.pyramids[0][level].size.width) + y1f = min(int(math.ceil(y1 / levelfactor)), self._dicom.pyramids[0][level].size.height) + bw = int(bw // levelfactor) + bh = int(bh // levelfactor) + tile = self._dicom.read_region( + (x0f, y0f), self._dicom.pyramids[0][level].level, (x1f - x0f, y1f - y0f)) + format = TILE_FORMAT_PIL + if tile.width < bw or tile.height < bh: + tile = _imageToNumpy(tile)[0] + tile = np.pad( + tile, + ((0, bh - tile.shape[0]), (0, bw - tile.shape[1]), (0, 0)), + 'constant', constant_values=0) + tile = _imageToPIL(tile) + if bw > self.tileWidth or bh > self.tileHeight: + tile = tile.resize((self.tileWidth, self.tileHeight)) + return self._outputTile(tile, format, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+ + +
+[docs] + def getAssociatedImagesList(self): + """ + Return a list of associated images. + + :return: the list of image keys. + """ + return [key for key in ['label', 'macro'] if self._getAssociatedImage(key)]
+ + + def _getAssociatedImage(self, imageKey): + """ + Get an associated image in PIL format. + + :param imageKey: the key of the associated image. + :return: the image in PIL format or None. + """ + keyMap = { + 'label': 'read_label', + 'macro': 'read_overview', + } + try: + return getattr(self._dicom, keyMap[imageKey])() + except Exception: + return None
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return DICOMFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return DICOMFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom/assetstore.html b/_modules/large_image_source_dicom/assetstore.html new file mode 100644 index 000000000..3e5524588 --- /dev/null +++ b/_modules/large_image_source_dicom/assetstore.html @@ -0,0 +1,210 @@ + + + + + + large_image_source_dicom.assetstore — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom.assetstore

+from girder import events
+from girder.api.v1.assetstore import Assetstore as AssetstoreResource
+from girder.constants import AssetstoreType
+from girder.models.assetstore import Assetstore
+from girder.utility.assetstore_utilities import setAssetstoreAdapter
+
+from .dicomweb_assetstore_adapter import DICOMWEB_META_KEY, DICOMwebAssetstoreAdapter
+from .rest import DICOMwebAssetstoreResource
+
+__all__ = [
+    'DICOMWEB_META_KEY',
+    'DICOMwebAssetstoreAdapter',
+    'load',
+]
+
+
+def createAssetstore(event):
+    """
+    When an assetstore is created, make sure it has a well-formed DICOMweb
+    information record.
+
+    :param event: Girder rest.post.assetstore.before event.
+    """
+    params = event.info['params']
+
+    if params.get('type') == AssetstoreType.DICOMWEB:
+        event.addResponse(Assetstore().save({
+            'type': AssetstoreType.DICOMWEB,
+            'name': params.get('name'),
+            DICOMWEB_META_KEY: {
+                'url': params['url'],
+                'qido_prefix': params.get('qido_prefix'),
+                'wado_prefix': params.get('wado_prefix'),
+                'auth_type': params.get('auth_type'),
+                'auth_token': params.get('auth_token'),
+            },
+        }))
+        event.preventDefault()
+
+
+def updateAssetstore(event):
+    """
+    When an assetstore is updated, make sure the result has a well-formed set
+    of DICOMweb information.
+
+    :param event: Girder assetstore.update event.
+    """
+    params = event.info['params']
+    store = event.info['assetstore']
+
+    if store['type'] == AssetstoreType.DICOMWEB:
+        store[DICOMWEB_META_KEY] = {
+            'url': params['url'],
+            'qido_prefix': params.get('qido_prefix'),
+            'wado_prefix': params.get('wado_prefix'),
+            'auth_type': params.get('auth_type'),
+            'auth_token': params.get('auth_token'),
+        }
+
+
+
+[docs] +def load(info): + """ + Load the plugin into Girder. + + :param info: a dictionary of plugin information. The name key contains the + name of the plugin according to Girder. + """ + AssetstoreType.DICOMWEB = 'dicomweb' + setAssetstoreAdapter(AssetstoreType.DICOMWEB, DICOMwebAssetstoreAdapter) + events.bind('assetstore.update', 'dicomweb_assetstore', updateAssetstore) + events.bind('rest.post.assetstore.before', 'dicomweb_assetstore', + createAssetstore) + + (AssetstoreResource.createAssetstore.description + .param('url', 'The base URL for the DICOMweb server (for DICOMweb)', + required=False) + .param('qido_prefix', 'The QIDO URL prefix for the server, if needed (for DICOMweb)', + required=False) + .param('wado_prefix', 'The WADO URL prefix for the server, if needed (for DICOMweb)', + required=False) + .param('auth_type', + 'The authentication type required for the server, if needed (for DICOMweb)', + required=False) + .param('auth_token', + 'Token for authentication if needed (for DICOMweb)', + required=False)) + + info['apiRoot'].dicomweb_assetstore = DICOMwebAssetstoreResource()
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.html b/_modules/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.html new file mode 100644 index 000000000..91946a168 --- /dev/null +++ b/_modules/large_image_source_dicom/assetstore/dicomweb_assetstore_adapter.html @@ -0,0 +1,760 @@ + + + + + + large_image_source_dicom.assetstore.dicomweb_assetstore_adapter — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom.assetstore.dicomweb_assetstore_adapter

+import json
+
+import cherrypy
+import requests
+from large_image_source_dicom.dicom_tags import dicom_key_to_tag
+from large_image_source_dicom.dicomweb_utils import get_dicomweb_metadata
+from requests.exceptions import HTTPError
+
+from girder.api.rest import setContentDisposition, setResponseHeader
+from girder.exceptions import ValidationException
+from girder.models.file import File
+from girder.models.folder import Folder
+from girder.models.item import Item
+from girder.utility.abstract_assetstore_adapter import AbstractAssetstoreAdapter
+
+DICOMWEB_META_KEY = 'dicomweb_meta'
+
+BUF_SIZE = 65536
+
+
+
+[docs] +class DICOMwebAssetstoreAdapter(AbstractAssetstoreAdapter): + """ + This defines the interface to be used by all assetstore adapters. + """ + + def __init__(self, assetstore): + super().__init__(assetstore) + +
+[docs] + @staticmethod + def validateInfo(doc): + # Ensure that the assetstore is marked read-only + doc['readOnly'] = True + + required_fields = [ + 'url', + ] + + info = doc.get(DICOMWEB_META_KEY, {}) + + for field in required_fields: + if field not in info: + raise ValidationException('Missing field: ' + field) + + # If these are empty, they need to be converted to None + convert_empty_fields_to_none = [ + 'qido_prefix', + 'wado_prefix', + 'auth_type', + ] + + for field in convert_empty_fields_to_none: + if isinstance(info.get(field), str) and not info[field].strip(): + info[field] = None + + if info['auth_type'] == 'token' and not info.get('auth_token'): + msg = 'A token must be provided if the auth type is "token"' + raise ValidationException(msg) + + # Verify that we can connect to the server, if the authentication type + # allows it. + if info['auth_type'] in (None, 'token'): + study_instance_uid_tag = dicom_key_to_tag('StudyInstanceUID') + series_instance_uid_tag = dicom_key_to_tag('SeriesInstanceUID') + + client = _create_dicomweb_client(info) + # Try to search for series. If we get an http error, raise + # a validation exception. + try: + series = client.search_for_series( + limit=1, + fields=(study_instance_uid_tag, series_instance_uid_tag), + ) + except HTTPError as e: + msg = f'Failed to validate DICOMweb server settings: {e}' + raise ValidationException(msg) + + # If we found a series, then test the wado prefix as well + if series: + # The previous query should have obtained uids for a specific + # study and series. + study_uid = series[0][study_instance_uid_tag]['Value'][0] + series_uid = series[0][series_instance_uid_tag]['Value'][0] + try: + # Retrieve the metadata of this series as a wado prefix test + client.retrieve_series_metadata( + study_instance_uid=study_uid, + series_instance_uid=series_uid, + ) + except HTTPError as e: + msg = f'Failed to validate DICOMweb WADO prefix: {e}' + raise ValidationException(msg) + + return doc
+ + + @property + def assetstore_meta(self): + return self.assetstore[DICOMWEB_META_KEY] + +
+[docs] + def initUpload(self, upload): + msg = 'DICOMweb assetstores are import only.' + raise NotImplementedError(msg)
+ + +
+[docs] + def finalizeUpload(self, upload, file): + msg = 'DICOMweb assetstores are import only.' + raise NotImplementedError(msg)
+ + +
+[docs] + def deleteFile(self, file): + # We don't actually need to do anything special + pass
+ + +
+[docs] + def setContentHeaders(self, file, offset, endByte, contentDisposition=None): + """ + Sets the Content-Length, Content-Disposition, Content-Type, and also + the Content-Range header if this is a partial download. + + :param file: The file being downloaded. + :param offset: The start byte of the download. + :type offset: int + :param endByte: The end byte of the download (non-inclusive). + :type endByte: int or None + :param contentDisposition: Content-Disposition response header + disposition-type value, if None, Content-Disposition will + be set to 'attachment; filename=$filename'. + :type contentDisposition: str or None + """ + isRangeRequest = cherrypy.request.headers.get('Range') + setResponseHeader('Content-Type', file['mimeType']) + setContentDisposition(file['name'], contentDisposition or 'attachment') + + if file.get('size') is not None: + # Only set Content-Length and range request headers if we have a file size + size = file['size'] + if endByte is None or endByte > size: + endByte = size + + setResponseHeader('Content-Length', max(endByte - offset, 0)) + + if offset or endByte < size or isRangeRequest: + setResponseHeader('Content-Range', f'bytes {offset}-{endByte - 1}/{size}')
+ + +
+[docs] + def downloadFile(self, file, offset=0, headers=True, endByte=None, + contentDisposition=None, extraParameters=None, **kwargs): + + if headers: + setResponseHeader('Accept-Ranges', 'bytes') + self.setContentHeaders(file, offset, endByte, contentDisposition) + + def stream(): + # Perform the request + # Try a single-part download first. If that doesn't work, do multipart. + response = self._request_retrieve_instance_prefer_singlepart(file) + + bytes_read = 0 + for chunk in self._stream_retrieve_instance_response(response): + if bytes_read < offset: + # We haven't reached the start of the offset yet + bytes_needed = offset - bytes_read + if bytes_needed >= len(chunk): + # Skip over the whole chunk... + bytes_read += len(chunk) + continue + else: + # Discard all bytes before the offset + chunk = chunk[bytes_needed:] + bytes_read += bytes_needed + + if endByte is not None and bytes_read + len(chunk) >= endByte: + # We have reached the end... remove all bytes after endByte + chunk = chunk[:endByte - bytes_read] + if chunk: + yield chunk + + bytes_read += len(chunk) + break + + yield chunk + bytes_read += len(chunk) + + return stream
+ + + def _request_retrieve_instance_prefer_singlepart(self, file, transfer_syntax='*'): + # Try to perform a singlepart request. If it fails, perform a multipart request + # instead. + response = None + try: + response = self._request_retrieve_instance(file, multipart=False, + transfer_syntax=transfer_syntax) + except requests.HTTPError: + # If there is an HTTPError, the server might not accept single-part requests... + pass + + if self._is_singlepart_response(response): + return response + + # Perform the multipart request instead + return self._request_retrieve_instance(file, transfer_syntax=transfer_syntax) + + def _request_retrieve_instance(self, file, multipart=True, transfer_syntax='*'): + # Multipart requests are officially supported by the DICOMweb standard. + # Singlepart requests are not officially supported, but they are easier + # to work with. + # Google Healthcare API support it. + # See here: https://cloud.google.com/healthcare-api/docs/dicom#dicom_instances + + # Create the URL + client = _create_dicomweb_client(self.assetstore_meta) + url = self._create_retrieve_instance_url(client, file) + + # Build the headers + headers = {} + if multipart: + # This is officially supported by the DICOMweb standard. + headers['Accept'] = '; '.join(( + 'multipart/related', + 'type="application/dicom"', + f'transfer-syntax={transfer_syntax}', + )) + else: + # This is not officially supported by the DICOMweb standard, + # but it is easier to work with, and some servers such as + # Google Healthcare API support it. + # See here: https://cloud.google.com/healthcare-api/docs/dicom#dicom_instances + headers['Accept'] = f'application/dicom; transfer-syntax={transfer_syntax}' + + return client._http_get(url, headers=headers, stream=True) + + def _create_retrieve_instance_url(self, client, file): + from dicomweb_client.web import _Transaction + + dicom_uids = file['dicom_uids'] + study_uid = dicom_uids['study_uid'] + series_uid = dicom_uids['series_uid'] + instance_uid = dicom_uids['instance_uid'] + + return client._get_instances_url( + _Transaction.RETRIEVE, + study_uid, + series_uid, + instance_uid, + ) + + def _stream_retrieve_instance_response(self, response): + # Check if the original request asked for multipart data + if 'multipart/related' in response.request.headers.get('Accept', ''): + yield from self._stream_dicom_multipart_response(response) + else: + # The content should *only* contain the DICOM file + with response: + yield from response.iter_content(BUF_SIZE) + + def _extract_media_type_and_boundary(self, response): + content_type = response.headers['content-type'] + media_type, *ct_info = (ct.strip() for ct in content_type.split(';')) + boundary = None + for item in ct_info: + attr, _, value = item.partition('=') + if attr.lower() == 'boundary': + boundary = value.strip('"').encode() + break + + return media_type, boundary + + def _stream_dicom_multipart_response(self, response): + # The first part of this function was largely copied from dicomweb-client's + # _decode_multipart_message() function. But we can't use that function here + # because it relies on reading the whole DICOM file into memory. We want to + # avoid that and stream in chunks. + + # Split the content-type to find the media type and boundary. + media_type, boundary = self._extract_media_type_and_boundary(response) + if media_type.lower() != 'multipart/related': + msg = f'Unexpected media type: "{media_type}". Expected "multipart/related".' + raise ValueError(msg) + + # Ensure we have the multipart/related boundary. + # The beginning boundary and end boundary look slightly different (in my + # examples, beginning looks like '--{boundary}\r\n', and ending looks like + # '\r\n--{boundary}--'). But we skip over the beginning boundary anyways + # since it is before the message body. An end boundary might look like this: + # \r\n--50d7ccd118978542c422543a7156abfce929e7615bc024e533c85801cd77-- + if boundary is None: + content_type = response.headers['content-type'] + msg = f'Failed to locate boundary in content-type: {content_type}' + raise ValueError(msg) + + # Both dicomweb-client and requests-toolbelt check for + # the ending boundary exactly like so: + ending = b'\r\n--' + boundary + + # Sometimes, there are a few extra bytes after the ending, such + # as '--' and '\r\n'. Imaging Data Commons has '--\r\n' at the end. + # But we don't care about what comes after the ending. As soon as we + # encounter the ending, we are done. + ending_size = len(ending) + + # Make sure the buffer is at least large enough to contain the + # ending_size - 1, so that the ending cannot be split between more than 2 chunks. + buffer_size = max(BUF_SIZE, ending_size - 1) + + with response: + # Create our iterator + iterator = response.iter_content(buffer_size) + + # First, stream until we encounter the first `\r\n\r\n`, + # which denotes the end of the header section. + header_found = False + end_header_delimiter = b'\r\n\r\n' + for chunk in iterator: + if end_header_delimiter in chunk: + idx = chunk.index(end_header_delimiter) + # Save the first section of data. We will yield it later. + prev_chunk = chunk[idx + len(end_header_delimiter):] + header_found = True + break + + if not header_found: + msg = 'Failed to find header in response content' + raise ValueError(msg) + + # Now the header has been finished. Stream the data until + # we encounter the ending boundary or finish the data. + # The "prev_chunk" will start out set to the section right after the header. + for chunk in iterator: + # Ensure the chunk is large enough to contain the ending_size - 1, so + # we can be sure the ending won't be split across more than 2 chunks. + while len(chunk) < ending_size - 1: + try: + chunk += next(iterator) + except StopIteration: + break + + # Check if the ending is split between the previous and current chunks. + if ending in prev_chunk + chunk[:ending_size - 1]: + # We found the ending! Remove the ending boundary and return. + data = prev_chunk + chunk[:ending_size - 1] + yield data.split(ending, maxsplit=1)[0] + return + + if prev_chunk: + yield prev_chunk + + prev_chunk = chunk + + # We did not find the ending while looping. + # Check if it is in the final chunk. + if ending in prev_chunk: + # Found the ending in the final chunk. + yield prev_chunk.split(ending, maxsplit=1)[0] + return + + # We should have encountered the ending earlier and returned + msg = 'Failed to find ending boundary in response content' + raise ValueError(msg) + + def _infer_file_size(self, file): + # Try various methods to infer the file size, without streaming the + # whole file. Returns the file size if successful, or `None` if unsuccessful. + if file.get('size') is not None: + # The file size was already determined. + return file['size'] + + # Only method currently is inferring from single-part content_length + return self._infer_file_size_singlepart_content_length(file) + + def _is_singlepart_response(self, response): + if response is None: + return False + + content_type = response.headers.get('Content-Type') + return ( + response.status_code == 200 and + not any(x in content_type for x in ('multipart/related', 'boundary')) + ) + + def _infer_file_size_singlepart_content_length(self, file): + # First, try to see if single-part requests work, and if the Content-Length + # is returned. This works for Google Healthcare API. + try: + response = self._request_retrieve_instance(file, multipart=False) + except requests.HTTPError: + # If there is an HTTPError, the server might not accept single-part requests... + return + + if not self._is_singlepart_response(response): + # Does not support single-part requests... + return + + content_length = response.headers.get('Content-Length') + if not content_length: + # The server did not return a Content-Length + return + + try: + # The DICOM file size is equal to the Content-Length + return int(content_length) + except ValueError: + return + + def _importData(self, parent, parentType, params, progress, user): + if parentType not in ('folder', 'user', 'collection'): + msg = f'Invalid parent type: {parentType}' + raise ValidationException(msg) + + limit = params.get('limit') + search_filters = params.get('search_filters', {}) + + meta = self.assetstore_meta + + client = _create_dicomweb_client(meta) + + study_uid_key = dicom_key_to_tag('StudyInstanceUID') + series_uid_key = dicom_key_to_tag('SeriesInstanceUID') + instance_uid_key = dicom_key_to_tag('SOPInstanceUID') + + # Search for studies. Apply the limit and search filters. + fields = [ + study_uid_key, + ] + if progress: + progress.update(message='Searching for studies...') + + studies_results = client.search_for_studies( + limit=limit, + fields=fields, + search_filters=search_filters, + ) + + # Search for all series in the returned studies. + fields = [ + study_uid_key, + series_uid_key, + ] + if progress: + progress.update(message='Searching for series...') + + series_results = [] + for study in studies_results: + study_uid = study[study_uid_key]['Value'][0] + series_results += client.search_for_series(study_uid, fields=fields) + + # Create folders for each study, items for each series, and files for each instance. + items = [] + for i, result in enumerate(series_results): + if progress: + progress.update(total=len(series_results), current=i, + message='Importing series...') + + study_uid = result[study_uid_key]['Value'][0] + series_uid = result[series_uid_key]['Value'][0] + + # Create a folder for the study, and an item for the series + folder = Folder().createFolder(parent, parentType=parentType, + name=study_uid, creator=user, + reuseExisting=True) + item = Item().createItem(name=series_uid, creator=user, folder=folder, + reuseExisting=True) + + # Set the DICOMweb metadata + item['dicomweb_meta'] = get_dicomweb_metadata(client, study_uid, series_uid) + item['dicom_uids'] = { + 'study_uid': study_uid, + 'series_uid': series_uid, + } + item = Item().save(item) + + instance_results = client.search_for_instances(study_uid, series_uid) + for instance in instance_results: + instance_uid = instance[instance_uid_key]['Value'][0] + + file = File().createFile( + name=f'{instance_uid}.dcm', + creator=user, + item=item, + reuseExisting=True, + assetstore=self.assetstore, + mimeType='application/dicom', + size=None, + saveFile=False, + ) + file['dicom_uids'] = { + 'study_uid': study_uid, + 'series_uid': series_uid, + 'instance_uid': instance_uid, + } + file['imported'] = True + + # Inferring the file size can take a long time, so don't + # do it right away, unless we figure out a way to make it faster. + # file['size'] = self._infer_file_size(file) + file = File().save(file) + + items.append(item) + + return items + +
+[docs] + def importData(self, parent, parentType, params, progress, user, **kwargs): + """ + Import DICOMweb WSI instances from a DICOMweb server. + + :param parent: The parent object to import into. + :param parentType: The model type of the parent object. + :type parentType: str + :param params: Additional parameters required for the import process. + This dictionary may include the following keys: + + :limit: (optional) limit the number of studies imported. + :filters: (optional) a dictionary/JSON string of additional search + filters to use with dicomweb_client's `search_for_series()` + function. + + :type params: dict + :param progress: Object on which to record progress if possible. + :type progress: :py:class:`girder.utility.progress.ProgressContext` + :param user: The Girder user performing the import. + :type user: dict or None + :return: a list of items that were created + """ + # Validate the parameters + limit = params.get('limit') or None + if limit is not None: + error_msg = f'Invalid limit: {limit}' + try: + limit = int(limit) + except ValueError: + raise ValidationException(error_msg) + + if limit < 1: + raise ValidationException(error_msg) + + search_filters = params.get('filters', {}) + if isinstance(search_filters, str): + try: + search_filters = json.loads(search_filters) + except json.JSONDecodeError as e: + msg = f'Invalid filters: "{params.get("filters")}". {e}' + raise ValidationException(msg) + + items = self._importData( + parent, + parentType, + { + 'limit': limit, + 'search_filters': search_filters, + }, + progress, + user, + ) + + if not items: + msg = 'No studies matching the search filters were found' + raise ValidationException(msg) + + return items
+ + + @property + def auth_session(self): + return _create_auth_session(self.assetstore_meta) + +
+[docs] + def getFileSize(self, file): + # This function will compute the size of the DICOM file (a potentially + # expensive operation, since it may have to stream the whole file). + # The caller is expected to cache the result in file['size']. + # This function is called when the size is needed, such as the girder + # fuse mount code, and range requests. + if file.get('size') is not None: + # It has already been computed once. Return the cached size. + return file['size'] + + # Try to infer the file size without streaming, if possible. + size = self._infer_file_size(file) + if size: + return size + + # We must stream the whole file to get the file size... + size = 0 + response = self._request_retrieve_instance_prefer_singlepart(file) + for chunk in self._stream_retrieve_instance_response(response): + size += len(chunk) + + # This should get cached in file['size'] in File().updateSize(). + return size
+
+ + + +def _create_auth_session(meta): + auth_type = meta.get('auth_type') + if auth_type is None: + return None + + if auth_type == 'token': + return _create_token_auth_session(meta['auth_token']) + + msg = f'Unhandled auth type: {auth_type}' + raise NotImplementedError(msg) + + +def _create_token_auth_session(token): + s = requests.Session() + s.headers.update({'Authorization': f'Bearer {token}'}) + return s + + +def _create_dicomweb_client(meta): + from dicomweb_client.api import DICOMwebClient + + session = _create_auth_session(meta) + + # Make the DICOMwebClient + return DICOMwebClient( + url=meta['url'], + qido_url_prefix=meta.get('qido_prefix'), + wado_url_prefix=meta.get('wado_prefix'), + session=session, + ) +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom/assetstore/rest.html b/_modules/large_image_source_dicom/assetstore/rest.html new file mode 100644 index 000000000..78a8af8a1 --- /dev/null +++ b/_modules/large_image_source_dicom/assetstore/rest.html @@ -0,0 +1,200 @@ + + + + + + large_image_source_dicom.assetstore.rest — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom.assetstore.rest

+from girder.api import access
+from girder.api.describe import Description, autoDescribeRoute
+from girder.api.rest import Resource
+from girder.constants import TokenScope
+from girder.exceptions import RestException
+from girder.models.assetstore import Assetstore
+from girder.utility.model_importer import ModelImporter
+from girder.utility.progress import ProgressContext
+
+from .dicomweb_assetstore_adapter import DICOMwebAssetstoreAdapter
+
+
+
+[docs] +class DICOMwebAssetstoreResource(Resource): + def __init__(self): + super().__init__() + self.resourceName = 'dicomweb_assetstore' + self.route('POST', (':id', 'import'), self.importData) + + def _importData(self, assetstore, params, progress): + """ + + :param assetstore: the destination assetstore. + :param params: a dictionary of parameters including destinationId, + destinationType, progress, and filters. + """ + user = self.getCurrentUser() + + destinationType = params['destinationType'] + if destinationType not in ('folder', 'user', 'collection'): + msg = f'Invalid destinationType: {destinationType}' + raise RestException(msg) + + parent = ModelImporter.model(destinationType).load(params['destinationId'], force=True, + exc=True) + + return DICOMwebAssetstoreAdapter(assetstore).importData( + parent, + destinationType, + params, + progress, + user, + ) + +
+[docs] + @access.admin(scope=TokenScope.DATA_WRITE) + @autoDescribeRoute( + Description('Import references to DICOM objects from a DICOMweb server') + .modelParam('id', 'The ID of the assetstore representing the DICOMweb server.', + model=Assetstore) + .param('destinationId', 'The ID of the parent folder, collection, or user ' + 'in the Girder data hierarchy under which to import the files.') + .param('destinationType', 'The type of the parent object to import into.', + enum=('folder', 'user', 'collection'), + required=False, default='folder') + .param('limit', 'The maximum number of studies to import.', + required=False, default=None) + .param('filters', 'Any search parameters to filter the studies query.', + required=False, default='{}') + .param('progress', 'Whether to record progress on this operation.', + required=False, default=False, dataType='boolean') + .errorResponse() + .errorResponse('You are not an administrator.', 403), + ) + def importData(self, assetstore, destinationId, destinationType, limit, filters, progress): + user = self.getCurrentUser() + + with ProgressContext( + progress, user=user, title='Importing DICOM references', + ) as ctx: + return self._importData(assetstore, params={ + 'destinationId': destinationId, + 'destinationType': destinationType, + 'limit': limit, + 'filters': filters, + }, progress=ctx)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom/dicom_metadata.html b/_modules/large_image_source_dicom/dicom_metadata.html new file mode 100644 index 000000000..4c297677f --- /dev/null +++ b/_modules/large_image_source_dicom/dicom_metadata.html @@ -0,0 +1,229 @@ + + + + + + large_image_source_dicom.dicom_metadata — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom.dicom_metadata

+
+[docs] +def extract_dicom_metadata(dataset): + # Extract any metadata we want to display from the dataset + + metadata = {} + for field in TOP_LEVEL_METADATA_FIELDS: + if field not in dataset: + # This field is missing + continue + + element = dataset[field] + value = element.value + + if not value: + # This field is blank + continue + + if isinstance(value, list): + value = ', '.join(value) + + metadata[element.name] = str(value) + + # The specimens are complex and many layers deep + specimens = extract_specimen_metadata(dataset) + if specimens: + metadata['Specimens'] = specimens + + return metadata
+ + + +# These are the top-level metadata fields we will look for +# (if available on the DICOM object) +TOP_LEVEL_METADATA_FIELDS = [ + 'PatientID', + 'PatientName', + 'PatientSex', + 'PatientBirthDate', + + 'AccessionNumber', + 'StudyID', + 'StudyDate', + 'StudyTime', + + 'ClinicalTrialSponsorName', + 'ClinicalTrialProtocolID', + 'ClinicalTrialProtocolName', + 'ClinicalTrialSiteName', + + 'Manufacturer', + 'ManufacturerModelName', + 'DeviceSerialNumber', + 'SoftwareVersions', + + 'ReferringPhysicianName', + 'ModalitiesInStudy', +] + + +
+[docs] +def extract_specimen_metadata(dataset): + # Specimens are complex and many layers deep. + # This function tries to extract what we need from the specimens. + + output = [] + for specimen in getattr(dataset, 'SpecimenDescriptionSequence', []): + metadata = {} + if 'SpecimenIdentifier' in specimen: + metadata['Identifier'] = specimen.SpecimenIdentifier + + if 'SpecimenShortDescription' in specimen: + metadata['Description'] = specimen.SpecimenShortDescription + + structures = ', '.join( + x.CodeMeaning for x in getattr(specimen, 'PrimaryAnatomicStructureSequence', []) + ) + if structures: + metadata['Anatomical Structure'] = structures + + preps = [] + for prep in getattr(specimen, 'SpecimenPreparationSequence', []): + steps = {} + for step in getattr(prep, 'SpecimenPreparationStepContentItemSequence', []): + # Only extract entries that have both a name and a value + if (len(getattr(step, 'ConceptCodeSequence', [])) > 0 and + len(getattr(step, 'ConceptNameCodeSequence', [])) > 0): + name = step.ConceptNameCodeSequence[0].CodeMeaning + value = step.ConceptCodeSequence[0].CodeMeaning + if name in steps: + # There must be several values for this name. + # Turn it into a list instead. + if not isinstance(steps[name], list): + steps[name] = [steps[name]] + steps[name].append(value) + else: + steps[name] = value + + if steps: + preps.append(steps) + + if preps: + metadata['Specimen Preparation'] = preps + + if metadata: + output.append(metadata) + + return output
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom/dicom_tags.html b/_modules/large_image_source_dicom/dicom_tags.html new file mode 100644 index 000000000..5ef57d5a4 --- /dev/null +++ b/_modules/large_image_source_dicom/dicom_tags.html @@ -0,0 +1,133 @@ + + + + + + large_image_source_dicom.dicom_tags — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom.dicom_tags

+# Cache these so we only look them up once per run
+DICOM_TAGS = {}
+
+
+
+[docs] +def dicom_key_to_tag(key): + if key not in DICOM_TAGS: + import pydicom + from pydicom.tag import Tag + DICOM_TAGS[key] = Tag(pydicom.datadict.tag_for_keyword(key)).json_key + + return DICOM_TAGS[key]
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom/dicomweb_utils.html b/_modules/large_image_source_dicom/dicomweb_utils.html new file mode 100644 index 000000000..3087b2e93 --- /dev/null +++ b/_modules/large_image_source_dicom/dicomweb_utils.html @@ -0,0 +1,191 @@ + + + + + + large_image_source_dicom.dicomweb_utils — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom.dicomweb_utils

+from large_image_source_dicom.dicom_metadata import extract_dicom_metadata
+from large_image_source_dicom.dicom_tags import dicom_key_to_tag
+
+
+
+[docs] +def get_dicomweb_metadata(client, study_uid, series_uid): + # Many series-level metadata items are available if we explicitly + # request for them in the `search_for_series()` calls. + # However, some things are not available in the series-level + # metadata - in particular, the specimen information is only + # on the instance-level metadata. + # It seems that, for the most part, all WSI DICOM instances in a + # series have virtually identical metadata (except one or two things, + # such as the suffix on the serial number, and sometimes one of the + # items in the specimen metadata). + # We will do as the SLIM viewer does: grab a single volume instance + # and use that for the metadata. + volume_metadata = get_first_wsi_volume_metadata(client, study_uid, series_uid) + if not volume_metadata: + # No metadata + return None + + from pydicom import Dataset + dataset = Dataset.from_json(volume_metadata) + return extract_dicom_metadata(dataset)
+ + + +
+[docs] +def get_first_wsi_volume_metadata(client, study_uid, series_uid): + # Find the first WSI Volume and return the DICOMweb metadata + from wsidicom.uid import WSI_SOP_CLASS_UID + + image_type_tag = dicom_key_to_tag('ImageType') + instance_uid_tag = dicom_key_to_tag('SOPInstanceUID') + + # We can't include the SOPClassUID as a search filter because Imaging Data Commons + # produces an error if we do. Perform the filtering manually instead. + class_uid_tag = dicom_key_to_tag('SOPClassUID') + + fields = [ + image_type_tag, + instance_uid_tag, + class_uid_tag, + ] + wsi_instances = client.search_for_instances( + study_uid, series_uid, fields=fields) + + volume_instance = None + for instance in wsi_instances: + class_type = instance.get(class_uid_tag, {}).get('Value') + if not class_type or class_type[0] != WSI_SOP_CLASS_UID: + # Only look at WSI classes + continue + + image_type = instance.get(image_type_tag, {}).get('Value') + # It would be nice if we could have a search filter for this, but + # I didn't see one... + if image_type and len(image_type) > 2 and image_type[2] == 'VOLUME': + volume_instance = instance + break + + if not volume_instance: + # No volumes were found... + return None + + instance_uid = volume_instance[instance_uid_tag]['Value'][0] + + return client.retrieve_instance_metadata(study_uid, series_uid, instance_uid)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom/girder_plugin.html b/_modules/large_image_source_dicom/girder_plugin.html new file mode 100644 index 000000000..6129327ea --- /dev/null +++ b/_modules/large_image_source_dicom/girder_plugin.html @@ -0,0 +1,136 @@ + + + + + + large_image_source_dicom.girder_plugin — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom.girder_plugin

+from girder.plugin import GirderPlugin
+
+from . import assetstore
+
+
+
+[docs] +class DICOMwebPlugin(GirderPlugin): + DISPLAY_NAME = 'DICOMweb Plugin' + CLIENT_SOURCE_PATH = 'web_client' + +
+[docs] + def load(self, info): + assetstore.load(info)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dicom/girder_source.html b/_modules/large_image_source_dicom/girder_source.html new file mode 100644 index 000000000..f9fa5ab82 --- /dev/null +++ b/_modules/large_image_source_dicom/girder_source.html @@ -0,0 +1,210 @@ + + + + + + large_image_source_dicom.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dicom.girder_source

+from girder_large_image.girder_tilesource import GirderTileSource
+
+from girder.constants import AssetstoreType
+from girder.models.file import File
+from girder.models.folder import Folder
+from girder.models.item import Item
+from girder.utility import assetstore_utilities
+from large_image.exceptions import TileSourceError
+
+from . import DICOMFileTileSource, _lazyImportPydicom
+from .assetstore import DICOMWEB_META_KEY
+
+
+
+[docs] +class DICOMGirderTileSource(DICOMFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with an DICOM file or other files that + the dicomreader library can read. + """ + + cacheName = 'tilesource' + name = 'dicom' + + _mayHaveAdjacentFiles = True + + def _getAssetstore(self): + files = Item().childFiles(self.item, limit=1) + if not files: + return None + + assetstore_id = files[0].get('assetstoreId') + if not assetstore_id: + return None + + return File()._getAssetstoreModel(files[0]).load(assetstore_id) + + def _getLargeImagePath(self): + # Look at a single file and see what type of assetstore it came from + # If it came from a DICOMweb assetstore, then we will use that method. + assetstore = self._getAssetstore() + assetstore_type = assetstore['type'] if assetstore else None + if assetstore_type == getattr(AssetstoreType, 'DICOMWEB', '__undefined__'): + return self._getDICOMwebLargeImagePath(assetstore) + else: + return self._getFilesystemLargeImagePath() + + def _getFilesystemLargeImagePath(self): + filelist = [ + File().getLocalFilePath(file) for file in Item().childFiles(self.item) + if self._pathMightBeDicom(file['name'])] + if len(filelist) != 1: + return filelist + try: + _lazyImportPydicom().filereader.dcmread(filelist[0], stop_before_pixels=True) + except Exception as exc: + msg = f'File cannot be opened via dicom tile source ({exc}).' + raise TileSourceError(msg) + filelist = [] + folder = Folder().load(self.item['folderId'], force=True) + for item in Folder().childItems(folder): + if len(list(Item().childFiles(item, limit=2))) == 1: + file = next(Item().childFiles(item, limit=2)) + if self._pathMightBeDicom(File().getLocalFilePath(file)): + filelist.append(File().getLocalFilePath(file)) + return filelist + + def _getDICOMwebLargeImagePath(self, assetstore): + meta = assetstore[DICOMWEB_META_KEY] + item_uids = self.item['dicom_uids'] + + adapter = assetstore_utilities.getAssetstoreAdapter(assetstore) + + return { + 'url': meta['url'], + 'study_uid': item_uids['study_uid'], + 'series_uid': item_uids['series_uid'], + # The following are optional + 'qido_prefix': meta.get('qido_prefix'), + 'wado_prefix': meta.get('wado_prefix'), + 'session': adapter.auth_session, + } + + def _getDicomMetadata(self): + if self._isDicomWeb: + # This should have already been saved in the item + return self.item['dicomweb_meta'] + + # Return the parent result. This is a cached method. + return super()._getDicomMetadata()
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_dummy.html b/_modules/large_image_source_dummy.html new file mode 100644 index 000000000..3c224c885 --- /dev/null +++ b/_modules/large_image_source_dummy.html @@ -0,0 +1,196 @@ + + + + + + large_image_source_dummy — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_dummy

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+from large_image.constants import SourcePriority
+from large_image.tilesource import TileSource
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+
+[docs] +class DummyTileSource(TileSource): + name = 'dummy' + extensions = { + None: SourcePriority.MANUAL, + } + + def __init__(self, *args, **kwargs): + super().__init__() + self.tileWidth = 0 + self.tileHeight = 0 + self.levels = 0 + self.sizeX = 0 + self.sizeY = 0 + +
+[docs] + @classmethod + def canRead(cls, *args, **kwargs): + return True
+ + +
+[docs] + def getTile(self, x, y, z, **kwargs): + return b''
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return DummyTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return DummyTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_gdal.html b/_modules/large_image_source_gdal.html new file mode 100644 index 000000000..2c308e3b6 --- /dev/null +++ b/_modules/large_image_source_gdal.html @@ -0,0 +1,1179 @@ + + + + + + large_image_source_gdal — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_gdal

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import math
+import os
+import pathlib
+import struct
+import tempfile
+import threading
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import numpy as np
+import PIL.Image
+
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import (TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY,
+                                   TILE_FORMAT_PIL, SourcePriority,
+                                   TileOutputMimeTypes)
+from large_image.exceptions import (TileSourceError,
+                                    TileSourceFileNotFoundError,
+                                    TileSourceInefficientError)
+from large_image.tilesource.geo import (GDALBaseFileTileSource,
+                                        ProjUnitsAcrossLevel0,
+                                        ProjUnitsAcrossLevel0_MaxSize)
+from large_image.tilesource.utilities import JSONDict, _gdalParameters, _vipsCast, _vipsParameters
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+gdal = None
+gdal_array = None
+gdalconst = None
+osr = None
+pyproj = None
+
+
+def _lazyImport():
+    """
+    Import the gdal module.  This is done when needed rather than in the
+    module initialization because it is slow.
+    """
+    global gdal, gdal_array, gdalconst, osr, pyproj
+
+    if gdal is None:
+        try:
+            from osgeo import gdal, gdal_array, gdalconst, osr
+
+            try:
+                gdal.UseExceptions()
+            except Exception:
+                pass
+
+            # isort: off
+
+            # pyproj stopped supporting older pythons, so on those versions its
+            # database is aging; as such, if on those older versions of python
+            # if it is imported before gdal, there can be a database version
+            # conflict; importing after gdal avoids this.
+            import pyproj
+
+            # isort: on
+        except ImportError:
+            msg = 'gdal module not found.'
+            raise TileSourceError(msg)
+
+
+
+[docs] +class GDALFileTileSource(GDALBaseFileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to geospatial files. + """ + + cacheName = 'tilesource' + name = 'gdal' + + def __init__(self, path, projection=None, unitsPerPixel=None, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + :param projection: None to use pixel space, otherwise a proj4 + projection string or a case-insensitive string of the form + 'EPSG:<epsg number>'. If a string and case-insensitively prefixed + with 'proj4:', that prefix is removed. For instance, + 'proj4:EPSG:3857', 'PROJ4:+init=epsg:3857', and '+init=epsg:3857', + and 'EPSG:3857' are all equivalent. + :param unitsPerPixel: The size of a pixel at the 0 tile size. Ignored + if the projection is None. For projections, None uses the default, + which is the distance between (-180,0) and (180,0) in EPSG:4326 + converted to the projection divided by the tile size. Proj4 + projections that are not latlong (is_geographic is False) must + specify unitsPerPixel. + """ + super().__init__(path, **kwargs) + _lazyImport() + + self.addKnownExtensions() + self._bounds = {} + self._largeImagePath = self._getLargeImagePath() + try: + self.dataset = gdal.Open(self._largeImagePath, gdalconst.GA_ReadOnly) + except RuntimeError: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + msg = 'File cannot be opened via GDAL' + raise TileSourceError(msg) + self._getDatasetLock = threading.RLock() + self.tileSize = 256 + self.tileWidth = self.tileSize + self.tileHeight = self.tileSize + if projection and projection.lower().startswith('epsg:'): + projection = projection.lower() + if projection and not isinstance(projection, bytes): + projection = projection.encode() + self.projection = projection + try: + with self._getDatasetLock: + self.sourceSizeX = self.sizeX = self.dataset.RasterXSize + self.sourceSizeY = self.sizeY = self.dataset.RasterYSize + except AttributeError as exc: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + raise TileSourceError('File cannot be opened via GDAL: %r' % exc) + is_netcdf = self._checkNetCDF() + try: + scale = self.getPixelSizeInMeters() + except RuntimeError as exc: + raise TileSourceError('File cannot be opened via GDAL: %r' % exc) + if (self.projection or self._getDriver() in { + 'PNG', + }) and not scale and not is_netcdf: + msg = ('File does not have a projected scale, so will not be ' + 'opened via GDAL with a projection.') + raise TileSourceError(msg) + self.sourceLevels = self.levels = int(max(0, math.ceil(max( + math.log(float(self.sizeX) / self.tileWidth), + math.log(float(self.sizeY) / self.tileHeight)) / math.log(2))) + 1) + self._unitsPerPixel = unitsPerPixel + if self.projection: + self._initWithProjection(unitsPerPixel) + self._getPopulatedLevels() + self._getTileLock = threading.Lock() + self._setDefaultStyle() + + def _getDriver(self): + """ + Get the GDAL driver used to read this dataset. + + :returns: The name of the driver. + """ + if not hasattr(self, '_driver'): + with self._getDatasetLock: + if not self.dataset or not self.dataset.GetDriver(): + self._driver = None + else: + self._driver = self.dataset.GetDriver().ShortName + return self._driver + + def _checkNetCDF(self): + if self._getDriver() == 'netCDF': + msg = 'netCDF file will not be read via GDAL source' + raise TileSourceError(msg) + return False + + def _getPopulatedLevels(self): + try: + with self._getDatasetLock: + self._populatedLevels = 1 + self.dataset.GetRasterBand(1).GetOverviewCount() + except Exception: + pass + + def _scanForMinMax(self, dtype, frame=None, analysisSize=1024, onlyMinMax=True): + frame = frame or 0 + bandInfo = self.getBandInformation() + if (not frame and onlyMinMax and all( + band.get('min') is not None and band.get('max') is not None + for band in bandInfo.values())): + with self._getDatasetLock: + dtype = gdal_array.GDALTypeCodeToNumericTypeCode( + self.dataset.GetRasterBand(1).DataType) + self._bandRanges[0] = { + 'min': np.array([band['min'] for band in bandInfo.values()], dtype=dtype), + 'max': np.array([band['max'] for band in bandInfo.values()], dtype=dtype), + } + else: + kwargs = {} + if self.projection: + bounds = self.getBounds(self.projection) + kwargs = {'region': { + 'left': bounds['xmin'], + 'top': bounds['ymax'], + 'right': bounds['xmax'], + 'bottom': bounds['ymin'], + 'units': 'projection', + }} + super(GDALFileTileSource, GDALFileTileSource)._scanForMinMax( + self, dtype=dtype, frame=frame, analysisSize=analysisSize, + onlyMinMax=onlyMinMax, **kwargs) + # Add the maximum range of the data type to the end of the band + # range list. This changes autoscaling behavior. For non-integer + # data types, this adds the range [0, 1]. + band_frame = self._bandRanges[frame] + try: + # only valid for integer dtypes + range_max = np.iinfo(band_frame['max'].dtype).max + except ValueError: + range_max = 1 + band_frame['min'] = np.append(band_frame['min'], 0) + band_frame['max'] = np.append(band_frame['max'], range_max) + + def _initWithProjection(self, unitsPerPixel=None): + """ + Initialize aspects of the class when a projection is set. + """ + inProj = self._proj4Proj('epsg:4326') + # Since we already converted to bytes decoding is safe here + outProj = self._proj4Proj(self.projection) + if outProj.crs.is_geographic: + msg = ('Projection must not be geographic (it needs to use linear ' + 'units, not longitude/latitude).') + raise TileSourceError(msg) + if unitsPerPixel: + self.unitsAcrossLevel0 = float(unitsPerPixel) * self.tileSize + else: + self.unitsAcrossLevel0 = ProjUnitsAcrossLevel0.get(self.projection) + if self.unitsAcrossLevel0 is None: + # If unitsPerPixel is not specified, the horizontal distance + # between -180,0 and +180,0 is used. Some projections (such as + # stereographic) will fail in this case; they must have a + # unitsPerPixel specified. + equator = pyproj.Transformer.from_proj(inProj, outProj, always_xy=True).transform( + [-180, 180], [0, 0]) + self.unitsAcrossLevel0 = abs(equator[0][1] - equator[0][0]) + if not self.unitsAcrossLevel0: + msg = 'unitsPerPixel must be specified for this projection' + raise TileSourceError(msg) + if len(ProjUnitsAcrossLevel0) >= ProjUnitsAcrossLevel0_MaxSize: + ProjUnitsAcrossLevel0.clear() + ProjUnitsAcrossLevel0[self.projection] = self.unitsAcrossLevel0 + # This was + # self.projectionOrigin = pyproj.transform(inProj, outProj, 0, 0) + # but for consistency, it should probably always be (0, 0). Whatever + # renders the map would need the same offset as used here. + self.projectionOrigin = (0, 0) + # Calculate values for this projection + self.levels = int(max(int(math.ceil( + math.log(self.unitsAcrossLevel0 / self.getPixelSizeInMeters() / self.tileWidth) / + math.log(2))) + 1, 1)) + # Report sizeX and sizeY as the whole world + self.sizeX = 2 ** (self.levels - 1) * self.tileWidth + self.sizeY = 2 ** (self.levels - 1) * self.tileHeight + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs): + return super(GDALFileTileSource, GDALFileTileSource).getLRUHash( + *args, **kwargs) + ',%s,%s' % ( + kwargs.get('projection', args[1] if len(args) >= 2 else None), + kwargs.get('unitsPerPixel', args[3] if len(args) >= 4 else None))
+ + +
+[docs] + def getState(self): + return super().getState() + ',%s,%s' % ( + self.projection, self._unitsPerPixel)
+ + +
+[docs] + def getProj4String(self): + """ + Returns proj4 string for the given dataset + + :returns: The proj4 string or None. + """ + with self._getDatasetLock: + if self.dataset.GetGCPs() and self.dataset.GetGCPProjection(): + wkt = self.dataset.GetGCPProjection() + else: + wkt = self.dataset.GetProjection() + if not wkt: + if (self.dataset.GetGeoTransform(can_return_null=True) or + hasattr(self, '_netcdf') or self._getDriver() in {'NITF'}): + return 'epsg:4326' + return + proj = osr.SpatialReference() + proj.ImportFromWkt(wkt) + return proj.ExportToProj4()
+ + + def _getGeoTransform(self): + """ + Get the GeoTransform. If GCPs are used, get the appropriate transform + for those. + + :returns: a six-component array with the transform + """ + with self._getDatasetLock: + gt = self.dataset.GetGeoTransform() + if (self.dataset.GetGCPProjection() and self.dataset.GetGCPs()): + gt = gdal.GCPsToGeoTransform(self.dataset.GetGCPs()) + return gt + + @staticmethod + def _proj4Proj(proj): + """ + Return a pyproj.Proj based on either a binary or unicode string. + + :param proj: a binary or unicode projection string. + :returns: a proj4 projection object. None if the specified projection + cannot be created. + """ + _lazyImport() + + if isinstance(proj, bytes): + proj = proj.decode() + if not isinstance(proj, str): + return + if proj.lower().startswith('proj4:'): + proj = proj.split(':', 1)[1] + if proj.lower().startswith('epsg:'): + proj = proj.lower() + return pyproj.Proj(proj) + +
+[docs] + def toNativePixelCoordinates(self, x, y, proj=None, roundResults=True): + """ + Convert a coordinate in the native projection (self.getProj4String) to + pixel coordinates. + + :param x: the x coordinate it the native projection. + :param y: the y coordinate it the native projection. + :param proj: input projection. None to use the source's projection. + :param roundResults: if True, round the results to the nearest pixel. + :return: (x, y) the pixel coordinate. + """ + if proj is None: + proj = self.projection + # convert to the native projection + inProj = self._proj4Proj(proj) + outProj = self._proj4Proj(self.getProj4String()) + px, py = pyproj.Transformer.from_proj(inProj, outProj, always_xy=True).transform(x, y) + # convert to native pixel coordinates + gt = self._getGeoTransform() + d = gt[2] * gt[4] - gt[1] * gt[5] + x = (gt[0] * gt[5] - gt[2] * gt[3] - gt[5] * px + gt[2] * py) / d + y = (gt[1] * gt[3] - gt[0] * gt[4] + gt[4] * px - gt[1] * py) / d + if roundResults: + x = int(round(x)) + y = int(round(y)) + return x, y
+ + + def _convertProjectionUnits(self, left, top, right, bottom, width, height, + units, **kwargs): + """ + Given bound information and a units string that consists of a proj4 + projection (starts with `'proj4:'`, `'epsg:'`, `'+proj='` or is an + enumerated value like `'wgs84'`), convert the bounds to either pixel or + the class projection coordinates. + + :param left: the left edge (inclusive) of the region to process. + :param top: the top edge (inclusive) of the region to process. + :param right: the right edge (exclusive) of the region to process. + :param bottom: the bottom edge (exclusive) of the region to process. + :param width: the width of the region to process. Ignored if both + left and right are specified. + :param height: the height of the region to process. Ignores if both + top and bottom are specified. + :param units: either 'projection', a string starting with 'proj4:', + 'epsg:', or '+proj=' or a enumerated value like 'wgs84', or one of + the super's values. + :param kwargs: optional parameters. + :returns: left, top, right, bottom, units. The new bounds in the + either pixel or class projection units. + """ + if not kwargs.get('unitsWH') or kwargs.get('unitsWH') == units: + if left is None and right is not None and width is not None: + left = right - width + if right is None and left is not None and width is not None: + right = left + width + if top is None and bottom is not None and height is not None: + top = bottom - height + if bottom is None and top is not None and height is not None: + bottom = top + height + if (left is None and right is None) or (top is None and bottom is None): + msg = ('Cannot convert from projection unless at least one of ' + 'left and right and at least one of top and bottom is ' + 'specified.') + raise TileSourceError(msg) + if not self.projection: + pleft, ptop = self.toNativePixelCoordinates( + right if left is None else left, + bottom if top is None else top, + units) + pright, pbottom = self.toNativePixelCoordinates( + left if right is None else right, + top if bottom is None else bottom, + units) + units = 'base_pixels' + else: + inProj = self._proj4Proj(units) + outProj = self._proj4Proj(self.projection) + transformer = pyproj.Transformer.from_proj(inProj, outProj, always_xy=True) + pleft, ptop = transformer.transform( + right if left is None else left, + bottom if top is None else top) + pright, pbottom = transformer.transform( + left if right is None else right, + top if bottom is None else bottom) + units = 'projection' + left = pleft if left is not None else None + top = ptop if top is not None else None + right = pright if right is not None else None + bottom = pbottom if bottom is not None else None + return left, top, right, bottom, units + +
+[docs] + def pixelToProjection(self, x, y, level=None): + """ + Convert from pixels back to projection coordinates. + + :param x, y: base pixel coordinates. + :param level: the level of the pixel. None for maximum level. + :returns: x, y in projection coordinates. + """ + if level is None: + level = self.levels - 1 + if not self.projection: + x *= 2 ** (self.levels - 1 - level) + y *= 2 ** (self.levels - 1 - level) + gt = self._getGeoTransform() + px = gt[0] + gt[1] * x + gt[2] * y + py = gt[3] + gt[4] * x + gt[5] * y + return px, py + xScale = 2 ** level * self.tileWidth + yScale = 2 ** level * self.tileHeight + x = x / xScale - 0.5 + y = 0.5 - y / yScale + x = x * self.unitsAcrossLevel0 + self.projectionOrigin[0] + y = y * self.unitsAcrossLevel0 + self.projectionOrigin[1] + return x, y
+ + +
+[docs] + def getBounds(self, srs=None): + """ + Returns bounds of the image. + + :param srs: the projection for the bounds. None for the default 4326. + :returns: an object with the four corners and the projection that was + used. None if we don't know the original projection. + """ + if srs not in self._bounds: + gt = self._getGeoTransform() + nativeSrs = self.getProj4String() + if not nativeSrs: + self._bounds[srs] = None + return + bounds = { + 'll': { + 'x': gt[0] + self.sourceSizeY * gt[2], + 'y': gt[3] + self.sourceSizeY * gt[5], + }, + 'ul': { + 'x': gt[0], + 'y': gt[3], + }, + 'lr': { + 'x': gt[0] + self.sourceSizeX * gt[1] + self.sourceSizeY * gt[2], + 'y': gt[3] + self.sourceSizeX * gt[4] + self.sourceSizeY * gt[5], + }, + 'ur': { + 'x': gt[0] + self.sourceSizeX * gt[1], + 'y': gt[3] + self.sourceSizeX * gt[4], + }, + 'srs': nativeSrs, + } + # Make sure geographic coordinates do not exceed their limits + if self._proj4Proj(nativeSrs).crs.is_geographic and srs: + try: + self._proj4Proj(srs)(0, 90, errcheck=True) + yBound = 90.0 + except RuntimeError: + yBound = 89.999999 + keys = ('ll', 'ul', 'lr', 'ur') + for key in keys: + bounds[key]['y'] = max(min(bounds[key]['y'], yBound), -yBound) + while any(bounds[key]['x'] > 180 for key in keys): + for key in keys: + bounds[key]['x'] -= 360 + while any(bounds[key]['x'] < -180 for key in keys): + for key in keys: + bounds[key]['x'] += 360 + if any(bounds[key]['x'] >= 180 for key in keys): + bounds['ul']['x'] = bounds['ll']['x'] = -180 + bounds['ur']['x'] = bounds['lr']['x'] = 180 + if srs and srs != nativeSrs: + inProj = self._proj4Proj(nativeSrs) + outProj = self._proj4Proj(srs) + keys = ('ll', 'ul', 'lr', 'ur') + pts = pyproj.Transformer.from_proj(inProj, outProj, always_xy=True).itransform([ + (bounds[key]['x'], bounds[key]['y']) for key in keys]) + for idx, pt in enumerate(pts): + key = keys[idx] + bounds[key]['x'] = pt[0] + bounds[key]['y'] = pt[1] + bounds['srs'] = srs.decode() if isinstance(srs, bytes) else srs + bounds['xmin'] = min(bounds['ll']['x'], bounds['ul']['x'], + bounds['lr']['x'], bounds['ur']['x']) + bounds['xmax'] = max(bounds['ll']['x'], bounds['ul']['x'], + bounds['lr']['x'], bounds['ur']['x']) + bounds['ymin'] = min(bounds['ll']['y'], bounds['ul']['y'], + bounds['lr']['y'], bounds['ur']['y']) + bounds['ymax'] = max(bounds['ll']['y'], bounds['ul']['y'], + bounds['lr']['y'], bounds['ur']['y']) + self._bounds[srs] = bounds + return self._bounds[srs]
+ + +
+[docs] + def getBandInformation(self, statistics=True, dataset=None, **kwargs): + """ + Get information about each band in the image. + + :param statistics: if True, compute statistics if they don't already + exist. Ignored: always treated as True. + :param dataset: the dataset. If None, use the main dataset. + :returns: a list of one dictionary per band. Each dictionary contains + known values such as interpretation, min, max, mean, stdev, nodata, + scale, offset, units, categories, colortable, maskband. + """ + if not getattr(self, '_bandInfo', None) or dataset: + with self._getDatasetLock: + cache = not dataset + if not dataset: + dataset = self.dataset + infoSet = JSONDict({}) + for i in range(dataset.RasterCount): + band = dataset.GetRasterBand(i + 1) + info = {} + try: + stats = band.GetStatistics(True, True) + # The statistics provide a min and max, so we don't + # fetch those separately + info.update(dict(zip(('min', 'max', 'mean', 'stdev'), stats))) + except RuntimeError: + self.logger.info('Failed to get statistics for band %d', i + 1) + info['nodata'] = band.GetNoDataValue() + info['scale'] = band.GetScale() + info['offset'] = band.GetOffset() + info['units'] = band.GetUnitType() + info['categories'] = band.GetCategoryNames() + interp = band.GetColorInterpretation() + info['interpretation'] = { + gdalconst.GCI_GrayIndex: 'gray', + gdalconst.GCI_PaletteIndex: 'palette', + gdalconst.GCI_RedBand: 'red', + gdalconst.GCI_GreenBand: 'green', + gdalconst.GCI_BlueBand: 'blue', + gdalconst.GCI_AlphaBand: 'alpha', + gdalconst.GCI_HueBand: 'hue', + gdalconst.GCI_SaturationBand: 'saturation', + gdalconst.GCI_LightnessBand: 'lightness', + gdalconst.GCI_CyanBand: 'cyan', + gdalconst.GCI_MagentaBand: 'magenta', + gdalconst.GCI_YellowBand: 'yellow', + gdalconst.GCI_BlackBand: 'black', + gdalconst.GCI_YCbCr_YBand: 'Y', + gdalconst.GCI_YCbCr_CbBand: 'Cb', + gdalconst.GCI_YCbCr_CrBand: 'Cr', + }.get(interp, interp) + if band.GetColorTable(): + info['colortable'] = [band.GetColorTable().GetColorEntry(pos) + for pos in range(band.GetColorTable().GetCount())] + if band.GetMaskBand(): + info['maskband'] = band.GetMaskBand().GetBand() or None + # Only keep values that aren't None or the empty string + infoSet[i + 1] = { + k: v for k, v in info.items() + if v not in (None, '') and not ( + isinstance(v, float) and + math.isnan(v) + ) + } + if not cache: + return infoSet + self._bandInfo = infoSet + return self._bandInfo
+ + + @property + def geospatial(self): + """ + This is true if the source has geospatial information. + """ + return bool( + self.dataset.GetProjection() or + (self.dataset.GetGCPProjection() and self.dataset.GetGCPs()) or + self.dataset.GetGeoTransform(can_return_null=True) or + hasattr(self, '_netcdf')) + +
+[docs] + def getMetadata(self): + metadata = super().getMetadata() + with self._getDatasetLock: + metadata.update({ + 'geospatial': self.geospatial, + 'sourceLevels': self.sourceLevels, + 'sourceSizeX': self.sourceSizeX, + 'sourceSizeY': self.sourceSizeY, + 'bounds': self.getBounds(self.projection), + 'projection': self.projection.decode() if isinstance( + self.projection, bytes) else self.projection, + 'sourceBounds': self.getBounds(), + 'bands': self.getBandInformation(), + }) + return metadata
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + result = JSONDict({}) + with self._getDatasetLock: + result['driverShortName'] = self.dataset.GetDriver().ShortName + result['driverLongName'] = self.dataset.GetDriver().LongName + result['fileList'] = self.dataset.GetFileList() + result['RasterXSize'] = self.dataset.RasterXSize + result['RasterYSize'] = self.dataset.RasterYSize + result['GeoTransform'] = self._getGeoTransform() + result['Projection'] = self.dataset.GetProjection() + result['proj4Projection'] = self.getProj4String() + result['GCPProjection'] = self.dataset.GetGCPProjection() + if self.dataset.GetGCPs(): + result['GCPs'] = [{ + 'id': gcp.Id, 'line': gcp.GCPLine, 'pixel': gcp.GCPPixel, + 'x': gcp.GCPX, 'y': gcp.GCPY, 'z': gcp.GCPZ} + for gcp in self.dataset.GetGCPs()] + result['Metadata'] = self.dataset.GetMetadata_List() + for key in ['IMAGE_STRUCTURE', 'SUBDATASETS', 'GEOLOCATION', 'RPC']: + metadatalist = self.dataset.GetMetadata_List(key) + if metadatalist: + result['Metadata_' + key] = metadatalist + if hasattr(self, '_netcdf'): + # To ensure all band information from all subdatasets in netcdf, + # we could do the following: + # for key in self._netcdf['datasets']: + # dataset = self._netcdf['datasets'][key] + # if 'bands' not in dataset: + # gdaldataset = gdal.Open(dataset['name'], gdalconst.GA_ReadOnly) + # dataset['bands'] = self.getBandInformation(gdaldataset) + # dataset['sizeX'] = gdaldataset.RasterXSize + # dataset['sizeY'] = gdaldataset.RasterYSize + result['netcdf'] = self._netcdf + return result
+ + + def _bandNumber(self, band, exc=True): # TODO: use super method? + """ + Given a band number or interpretation name, return a validated band + number. + + :param band: either -1, a positive integer, or the name of a band + interpretation that is present in the tile source. + :param exc: if True, raise an exception if no band matches. + :returns: a validated band, either 1 or a positive integer, or None if + no matching band and exceptions are not enabled. + """ + if hasattr(self, '_netcdf') and (':' in str(band) or str(band).isdigit()): + key = None + if ':' in str(band): + key, band = band.split(':', 1) + if str(band).isdigit(): + band = int(band) + else: + band = 1 + if not key or key == 'default': + key = self._netcdf.get('default', None) + if key is None: + return band + if key in self._netcdf['datasets']: + return (key, band) + bands = self.getBandInformation() + if not isinstance(band, int): + try: + band = next(bandIdx for bandIdx in sorted(bands) + if band == bands[bandIdx]['interpretation']) + except StopIteration: + pass + if hasattr(band, 'isdigit') and band.isdigit(): + band = int(band) + if band != -1 and band not in bands: + if exc: + msg = ('Band has to be a positive integer, -1, or a band ' + 'interpretation found in the source.') + raise TileSourceError(msg) + return None + return int(band) + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + if not self.projection: + self._xyzInRange(x, y, z) + factor = int(2 ** (self.levels - 1 - z)) + x0 = int(x * factor * self.tileWidth) + y0 = int(y * factor * self.tileHeight) + x1 = int(min(x0 + factor * self.tileWidth, self.sourceSizeX)) + y1 = int(min(y0 + factor * self.tileHeight, self.sourceSizeY)) + w = int(max(1, round((x1 - x0) / factor))) + h = int(max(1, round((y1 - y0) / factor))) + with self._getDatasetLock: + tile = self.dataset.ReadAsArray( + xoff=x0, yoff=y0, xsize=x1 - x0, ysize=y1 - y0, buf_xsize=w, buf_ysize=h) + else: + xmin, ymin, xmax, ymax = self.getTileCorners(z, x, y) + bounds = self.getBounds(self.projection) + if (xmin >= bounds['xmax'] or xmax <= bounds['xmin'] or + ymin >= bounds['ymax'] or ymax <= bounds['ymin']): + pilimg = PIL.Image.new('RGBA', (self.tileWidth, self.tileHeight)) + return self._outputTile( + pilimg, TILE_FORMAT_PIL, x, y, z, applyStyle=False, **kwargs) + res = (self.unitsAcrossLevel0 / self.tileSize) * (2 ** -z) + if not hasattr(self, '_warpSRS'): + self._warpSRS = (self.getProj4String(), + self.projection.decode()) + with self._getDatasetLock: + ds = gdal.Warp( + '', self.dataset, format='VRT', + srcSRS=self._warpSRS[0], dstSRS=self._warpSRS[1], + dstAlpha=True, + # Valid options are GRA_NearestNeighbour, GRA_Bilinear, + # GRA_Cubic, GRA_CubicSpline, GRA_Lanczos, GRA_Med, + # GRA_Mode, perhaps others; because we have some indexed + # datasets, generically, this should probably either be + # GRA_NearestNeighbour or GRA_Mode. + resampleAlg=gdal.GRA_NearestNeighbour, + multithread=True, + # We might get a speed-up with acceptable distortion if we + # set the polynomialOrder or ask for an optimal transform + # around the outputBounds. + polynomialOrder=1, + xRes=res, yRes=res, outputBounds=[xmin, ymin, xmax, ymax]) + tile = ds.ReadAsArray() + if len(tile.shape) == 3: + tile = np.rollaxis(tile, 0, 3) + return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+ + +
+[docs] + def getPixel(self, **kwargs): + """ + Get a single pixel from the current tile source. + + :param kwargs: optional arguments. Some options are region, output, + encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See + tileIterator. + :returns: a dictionary with the value of the pixel for each channel on + a scale of [0-255], including alpha, if available. This may + contain additional information. + """ + # TODO: netCDF - currently this will read the values from the + # default subdatatset; we may want it to read values from all + # subdatasets and the main raster bands (if they exist), and label the + # bands better + pixel = super().getPixel(includeTileRecord=True, **kwargs) + tile = pixel.pop('tile', None) + if tile: + # Coordinates in the max level tile + x, y = tile['gx'], tile['gy'] + if self.projection: + # convert to a scale of [-0.5, 0.5] + x = 0.5 + x / 2 ** (self.levels - 1) / self.tileWidth + y = 0.5 - y / 2 ** (self.levels - 1) / self.tileHeight + # convert to projection coordinates + x = self.projectionOrigin[0] + x * self.unitsAcrossLevel0 + y = self.projectionOrigin[1] + y * self.unitsAcrossLevel0 + # convert to native pixel coordinates + x, y = self.toNativePixelCoordinates(x, y) + if 0 <= int(x) < self.sizeX and 0 <= int(y) < self.sizeY: + with self._getDatasetLock: + for i in range(self.dataset.RasterCount): + band = self.dataset.GetRasterBand(i + 1) + try: + value = band.ReadRaster(int(x), int(y), 1, 1, buf_type=gdal.GDT_Float32) + if value: + pixel.setdefault('bands', {})[i + 1] = struct.unpack('f', value)[0] + except RuntimeError: + pass + return pixel
+ + + def _encodeTiledImageFromVips(self, vimg, iterInfo, image, **kwargs): + """ + Save a vips image as a tiled tiff. + + :param vimg: a vips image. + :param iterInfo: information about the region based on the tile + iterator. + :param image: a record with partial vips images and the current output + size. + + Additional parameters are available. + + :param compression: the internal compression format. This can handle + a variety of options similar to the converter utility. + :returns: a pathlib.Path of the output file and the output mime type. + """ + convertParams = _vipsParameters(defaultCompression='lzw', **kwargs) + convertParams.pop('pyramid', None) + vimg = _vipsCast(vimg, convertParams['compression'] in {'webp', 'jpeg'}) + gdalParams = _gdalParameters(defaultCompression='lzw', **kwargs) + for ch in range(image['channels']): + gdalParams += [ + '-b' if ch not in (1, 3) or ch + 1 != image['channels'] else '-mask', str(ch + 1)] + tl = self.pixelToProjection( + iterInfo['region']['left'], iterInfo['region']['top'], iterInfo['level']) + br = self.pixelToProjection( + iterInfo['region']['right'], iterInfo['region']['bottom'], iterInfo['level']) + gdalParams += [ + '-a_srs', + iterInfo['metadata']['bounds']['srs'], + '-a_ullr', + str(tl[0]), + str(tl[1]), + str(br[0]), + str(br[1]), + ] + fd, tempPath = tempfile.mkstemp('.tiff', 'tiledRegion_') + os.close(fd) + fd, outputPath = tempfile.mkstemp('.tiff', 'tiledGeoRegion_') + os.close(fd) + try: + vimg.write_to_file(tempPath, **convertParams) + ds = gdal.Open(tempPath, gdalconst.GA_ReadOnly) + gdal.Translate(outputPath, ds, options=gdalParams) + os.unlink(tempPath) + except Exception as exc: + try: + os.unlink(tempPath) + except Exception: + pass + try: + os.unlink(outputPath) + except Exception: + pass + raise exc + return pathlib.Path(outputPath), TileOutputMimeTypes['TILED'] + +
+[docs] + def getRegion(self, format=(TILE_FORMAT_IMAGE, ), **kwargs): + """ + Get a rectangular region from the current tile source. Aspect ratio is + preserved. If neither width nor height is given, the original size of + the highest resolution level is used. If both are given, the returned + image will be no larger than either size. + + :param format: the desired format or a tuple of allowed formats. + Formats are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, + TILE_FORMAT_IMAGE). If TILE_FORMAT_IMAGE, encoding may be + specified. + :param kwargs: optional arguments. Some options are region, output, + encoding, jpegQuality, jpegSubsampling, tiffCompression, fill. See + tileIterator. + :returns: regionData, formatOrRegionMime: the image data and either the + mime type, if the format is TILE_FORMAT_IMAGE, or the format. + """ + if not isinstance(format, (tuple, set, list)): + format = (format, ) + # The tile iterator handles determining the output region + iterInfo = self.tileIterator(format=TILE_FORMAT_NUMPY, resample=None, **kwargs).info + # Only use gdal.Warp of the original image if the region has not been + # styled. + useGDALWarp = ( + iterInfo and + not self._jsonstyle and + TILE_FORMAT_IMAGE in format and + kwargs.get('encoding') == 'TILED') + if not useGDALWarp: + return super().getRegion(format, **kwargs) + srs = self.projection or self.getProj4String() + tl = self.pixelToProjection( + iterInfo['region']['left'], iterInfo['region']['top'], iterInfo['level']) + br = self.pixelToProjection( + iterInfo['region']['right'], iterInfo['region']['bottom'], iterInfo['level']) + outWidth = iterInfo['output']['width'] + outHeight = iterInfo['output']['height'] + gdalParams = _gdalParameters(defaultCompression='lzw', **kwargs) + gdalParams += ['-t_srs', srs] if srs is not None else [ + '-to', 'SRC_METHOD=NO_GEOTRANSFORM'] + gdalParams += [ + '-te', str(tl[0]), str(br[1]), str(br[0]), str(tl[1]), + '-ts', str(int(math.floor(outWidth))), str(int(math.floor(outHeight))), + ] + + fd, outputPath = tempfile.mkstemp('.tiff', 'tiledGeoRegion_') + os.close(fd) + try: + self.logger.info('Using gdal warp %r', gdalParams) + ds = gdal.Open(self._largeImagePath, gdalconst.GA_ReadOnly) + gdal.Warp(outputPath, ds, options=gdalParams) + except Exception as exc: + try: + os.unlink(outputPath) + except Exception: + pass + raise exc + return pathlib.Path(outputPath), TileOutputMimeTypes['TILED']
+ + +
+[docs] + def validateCOG(self, check_tiled=True, full_check=False, strict=True, warn=True): + """Check if this image is a valid Cloud Optimized GeoTiff. + + This will raise a :class:`large_image.exceptions.TileSourceInefficientError` + if not a valid Cloud Optimized GeoTiff. Otherwise, returns True. + + Requires the ``osgeo_utils`` package. + + Parameters + ---------- + check_tiled : bool + Set to False to ignore missing tiling. + full_check : bool + Set to True to check tile/strip leader/trailer bytes. + Might be slow on remote files + strict : bool + Enforce warnings as exceptions. Set to False to only warn and not + raise exceptions. + warn : bool + Log any warnings + + """ + from osgeo_utils.samples.validate_cloud_optimized_geotiff import validate + + warnings, errors, details = validate( + self._largeImagePath, + check_tiled=check_tiled, + full_check=full_check, + ) + if errors: + raise TileSourceInefficientError(errors) + if strict and warnings: + raise TileSourceInefficientError(warnings) + if warn: + for warning in warnings: + self.logger.warning(warning) + return True
+ + +
+[docs] + @staticmethod + def isGeospatial(path): + """ + Check if a path is likely to be a geospatial file. + + :param path: The path to the file + :returns: True if geospatial. + """ + _lazyImport() + + try: + ds = gdal.Open(str(path), gdalconst.GA_ReadOnly) + except Exception: + return False + if ds: + if ds.GetGCPs() and ds.GetGCPProjection(): + return True + if ds.GetProjection(): + return True + if ds.GetGeoTransform(can_return_null=True): + return True + if ds.GetDriver().ShortName in {'NITF', 'netCDF'}: + return True + return False
+ + +
+[docs] + @classmethod + def addKnownExtensions(cls): + if not hasattr(cls, '_addedExtensions'): + _lazyImport() + + cls._addedExtensions = True + cls.extensions = cls.extensions.copy() + cls.mimeTypes = cls.mimeTypes.copy() + for idx in range(gdal.GetDriverCount()): + drv = gdal.GetDriver(idx) + if drv.GetMetadataItem(gdal.DCAP_RASTER): + drvexts = drv.GetMetadataItem(gdal.DMD_EXTENSIONS) + if drvexts is not None: + for ext in drvexts.split(): + if ext.lower() not in cls.extensions: + cls.extensions[ext.lower()] = SourcePriority.IMPLICIT + drvmimes = drv.GetMetadataItem(gdal.DMD_MIMETYPE) + if drvmimes is not None: + if drvmimes not in cls.mimeTypes: + cls.mimeTypes[drvmimes] = SourcePriority.IMPLICIT
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return GDALFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return GDALFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_gdal/girder_source.html b/_modules/large_image_source_gdal/girder_source.html new file mode 100644 index 000000000..e2a218a3c --- /dev/null +++ b/_modules/large_image_source_gdal/girder_source.html @@ -0,0 +1,185 @@ + + + + + + large_image_source_gdal.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_gdal.girder_source

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import re
+
+import packaging.version
+from girder_large_image.girder_tilesource import GirderTileSource
+from osgeo import gdal
+
+from girder import logger
+from girder.models.file import File
+
+from . import GDALFileTileSource
+
+
+
+[docs] +class GDALGirderTileSource(GDALFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items for gdal layers. + """ + + name = 'gdal' + cacheName = 'tilesource' + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs): + return GirderTileSource.getLRUHash(*args, **kwargs) + ',%s,%s' % ( + kwargs.get('projection', args[1] if len(args) >= 2 else None), + kwargs.get('unitsPerPixel', args[3] if len(args) >= 4 else None))
+ + + def _getLargeImagePath(self): + """ + GDAL can read directly from http/https/ftp via /vsicurl. If this + is a link file, try to use it. + """ + try: + largeImageFileId = self.item['largeImage']['fileId'] + largeImageFile = File().load(largeImageFileId, force=True) + if (packaging.version.parse(gdal.__version__) >= packaging.version.parse('2.1.3') and + largeImageFile.get('linkUrl') and + not largeImageFile.get('assetstoreId') and + re.match(r'(http(|s)|ftp)://', largeImageFile['linkUrl'])): + largeImagePath = '/vsicurl/' + largeImageFile['linkUrl'] + logger.info('Using %s' % largeImagePath) + return largeImagePath + except Exception: + pass + return GirderTileSource._getLargeImagePath(self)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_mapnik.html b/_modules/large_image_source_mapnik.html new file mode 100644 index 000000000..55d07efb8 --- /dev/null +++ b/_modules/large_image_source_mapnik.html @@ -0,0 +1,542 @@ + + + + + + large_image_source_mapnik — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_mapnik

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import functools
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import mapnik
+import PIL.Image
+from large_image_source_gdal import GDALFileTileSource
+from osgeo import gdal, gdalconst
+
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_PIL, SourcePriority
+from large_image.exceptions import TileSourceError
+from large_image.tilesource.utilities import JSONDict
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+mapnik.logger.set_severity(mapnik.severity_type.Debug)
+
+
+
+[docs] +class MapnikFileTileSource(GDALFileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to geospatial files. + """ + + cacheName = 'tilesource' + name = 'mapnik' + extensions = { + None: SourcePriority.LOW, + 'nc': SourcePriority.PREFERRED, # netcdf + # National Imagery Transmission Format + 'ntf': SourcePriority.HIGHER, + 'nitf': SourcePriority.HIGHER, + 'tif': SourcePriority.LOWER, + 'tiff': SourcePriority.LOWER, + 'vrt': SourcePriority.HIGHER, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'image/geotiff': SourcePriority.HIGHER, + 'image/tiff': SourcePriority.LOWER, + 'image/x-tiff': SourcePriority.LOWER, + } + + def __init__(self, path, projection=None, unitsPerPixel=None, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + :param projection: None to use pixel space, otherwise a proj4 + projection string or a case-insensitive string of the form + 'EPSG:<epsg number>'. If a string and case-insensitively prefixed + with 'proj4:', that prefix is removed. For instance, + 'proj4:EPSG:3857', 'PROJ4:+init=epsg:3857', and '+init=epsg:3857', + and 'EPSG:3857' are all equivalent. + :param style: if None, use the default style for the file. Otherwise, + this is a string with a json-encoded dictionary. The style is + ignored if it does not contain 'band' or 'bands'. In addition to + the base class parameters, the style can also contain the following + keys: + + scheme: one of the mapnik.COLORIZER_xxx values. Case + insensitive. Possible values are at least 'discrete', + 'linear', and 'exact'. This defaults to 'linear'. + composite: this is a string containing one of the mapnik + CompositeOp properties. It defaults to 'lighten'. + + :param unitsPerPixel: The size of a pixel at the 0 tile size. Ignored + if the projection is None. For projections, None uses the default, + which is the distance between (-180,0) and (180,0) in EPSG:4326 + converted to the projection divided by the tile size. Proj4 + projections that are not latlong (is_geographic is False) must + specify unitsPerPixel. + """ + if projection and projection.lower().startswith('epsg'): + projection = projection.lower() + super().__init__( + path, projection=projection, unitsPerPixel=unitsPerPixel, **kwargs) + + def _checkNetCDF(self): + """ + Check if this file is a netCDF file. If so, get some metadata about + available datasets. + + This assumes things about the projection that may not be true for all + netCDF files. It could also be extended to get better data about + time bounds and other series data and to prevent selecting subdatasets + that are not spatially appropriate. + """ + if self._getDriver() != 'netCDF': + return False + datasets = {} + with self._getDatasetLock: + for name, desc in self.dataset.GetSubDatasets(): + parts = desc.split(None, 2) + dataset = { + 'name': name, + 'desc': desc, + 'dim': [int(val) for val in parts[0].strip('[]').split('x')], + 'key': parts[1], + 'format': parts[2], + } + dataset['values'] = functools.reduce(lambda x, y: x * y, dataset['dim']) + datasets[dataset['key']] = dataset + if not len(datasets) and (not self.dataset.RasterCount or self.dataset.GetProjection()): + return False + self._netcdf = { + 'datasets': datasets, + 'metadata': self.dataset.GetMetadata_Dict(), + } + if not len(datasets): + try: + self.getBounds('epsg:3857') + except RuntimeError: + self._bounds.clear() + del self._netcdf + return False + with self._getDatasetLock: + if not self.dataset.RasterCount: + self._netcdf['default'] = sorted([( + not ds['key'].endswith('_bnds'), + 'character' not in ds['format'], + ds['values'], + len(ds['dim']), + ds['dim'], + ds['key']) for ds in datasets.values()])[-1][-1] + # The base netCDF file reports different dimensions than the + # subdatasets. For now, use the "best" subdataset's dimensions + dataset = self._netcdf['datasets'][self._netcdf['default']] + dataset['dataset'] = gdal.Open(dataset['name'], gdalconst.GA_ReadOnly) + self.sourceSizeX = self.sizeX = dataset['dataset'].RasterXSize + self.sourceSizeY = self.sizeY = dataset['dataset'].RasterYSize + + self.dataset = dataset['dataset'] # use for projection information + + if not hasattr(self, '_style'): + self._style = JSONDict({ + 'band': self._netcdf['default'] + ':1' if self._netcdf.get('default') else 1, + 'scheme': 'linear', + 'palette': ['#000000', '#ffffff'], + 'min': 'min', + 'max': 'max', + }) + return True + + def _setDefaultStyle(self): + """Don't inherit from GDAL tilesource.""" + with self._getTileLock: + if hasattr(self, '_mapnikMap'): + del self._mapnikMap + +
+[docs] + @staticmethod + def interpolateMinMax(start, stop, count): + """ + Returns interpolated values for a given + start, stop and count + + :returns: List of interpolated values + """ + try: + step = (float(stop) - float(start)) / (float(count) - 1) + except ValueError: + msg = 'Minimum and maximum values should be numbers, "auto", "min", or "max".' + raise TileSourceError(msg) + return [float(start + i * step) for i in range(count)]
+ + +
+[docs] + def getOneBandInformation(self, band): + if not isinstance(band, tuple): + bandInfo = super().getOneBandInformation(band) + else: # netcdf + with self._getDatasetLock: + dataset = self._netcdf['datasets'][band[0]] + if not dataset.get('bands'): + if not dataset.get('dataset'): + dataset['dataset'] = gdal.Open(dataset['name'], gdalconst.GA_ReadOnly) + dataset['bands'] = self.getBandInformation(dataset=dataset['dataset']) + bandInfo = dataset['bands'][band[1]] + bandInfo.setdefault('min', 0) + bandInfo.setdefault('max', 255) + return bandInfo
+ + + def _colorizerFromStyle(self, style): + """ + Add a specified style to a mapnik raster symbolizer. + + :param style: a style object. + :returns: a mapnik raster colorizer. + """ + try: + scheme = style.get('scheme', 'linear') + mapnik_scheme = getattr(mapnik, f'COLORIZER_{scheme.upper()}') + except AttributeError: + mapnik_scheme = mapnik.COLORIZER_DISCRETE + msg = 'Scheme has to be either "discrete" or "linear".' + raise TileSourceError(msg) + colorizer = mapnik.RasterColorizer(mapnik_scheme, mapnik.Color(0, 0, 0, 0)) + bandInfo = self.getOneBandInformation(style['band']) + minimum = style.get('min', 0) + maximum = style.get('max', 255) + minimum = bandInfo[minimum] if minimum in ('min', 'max') else minimum + maximum = bandInfo[maximum] if maximum in ('min', 'max') else maximum + if minimum == 'auto': + if not (0 <= bandInfo['min'] <= 255 and 1 <= bandInfo['max'] <= 255): + minimum = bandInfo['min'] + else: + minimum = 0 + if maximum == 'auto': + if not (0 <= bandInfo['min'] <= 255 and 1 <= bandInfo['max'] <= 255): + maximum = bandInfo['max'] + else: + maximum = 255 + if style.get('palette') == 'colortable': + for value, color in enumerate(bandInfo['colortable']): + colorizer.add_stop(value, mapnik.Color(*color)) + else: + colors = self.getHexColors(style.get('palette', ['#000000', '#ffffff'])) + if len(colors) < 2: + msg = 'A palette must have at least 2 colors.' + raise TileSourceError(msg) + values = self.interpolateMinMax(minimum, maximum, len(colors)) + for value, color in sorted(zip(values, colors)): + colorizer.add_stop(value, mapnik.Color(color)) + + return colorizer + + def _addStyleToMap(self, m, layerSrs, colorizer=None, band=-1, extent=None, + composite=None, nodata=None): + """ + Add a mapik raster symbolizer to a map. + + :param m: mapnik map. + :param layerSrs: the layer projection + :param colorizer: a mapnik colorizer. + :param band: an integer band number. -1 for default. + :param extent: the extent to use for the mapnik layer. + :param composite: the composite operation to use. This is one of + mapnik.CompositeOp.xxx, typically lighten or multiply. + :param nodata: the value to use for missing data or None to use all + data. + """ + styleName = 'Raster Style' + if band != -1: + styleName += ' ' + str(band) + rule = mapnik.Rule() + sym = mapnik.RasterSymbolizer() + if colorizer is not None: + sym.colorizer = colorizer + getattr(rule, 'symbols', getattr(rule, 'symbolizers', None)).append(sym) + style = mapnik.Style() + style.rules.append(rule) + if composite is not None: + style.comp_op = composite + m.append_style(styleName, style) + lyr = mapnik.Layer('layer') + lyr.srs = layerSrs + gdalpath = self._largeImagePath + gdalband = band + if hasattr(self, '_netcdf') and isinstance(band, tuple): + gdalband = band[1] + gdalpath = self._netcdf['datasets'][band[0]]['name'] + params = dict(base=None, file=gdalpath, band=gdalband, extent=extent, nodata=nodata) + params = {k: v for k, v in params.items() if v is not None} + lyr.datasource = mapnik.Gdal(**params) + lyr.styles.append(styleName) + m.layers.append(lyr) + +
+[docs] + def addStyle(self, m, layerSrs, extent=None): + """ + Attaches raster style option to mapnik raster layer and adds the layer + to the mapnik map. + + :param m: mapnik map. + :param layerSrs: the layer projection + :param extent: the extent to use for the mapnik layer. + """ + style = self._styleBands() + bands = self.getBandInformation() + if not len(style): + style.append({'band': -1}) + self.logger.debug( + 'mapnik addTile specified style: %r, used style %r', + getattr(self, 'style', None), style) + for styleBand in style: + if styleBand['band'] != -1: + colorizer = self._colorizerFromStyle(styleBand) + composite = getattr(mapnik.CompositeOp, styleBand.get( + 'composite', 'multiply' if styleBand['band'] == 'alpha' else 'lighten')) + nodata = styleBand.get('nodata') + if nodata == 'auto': + nodata = bands.get('nodata') + else: + colorizer = None + composite = None + nodata = None + self._addStyleToMap( + m, layerSrs, colorizer, styleBand['band'], extent, composite, nodata)
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, **kwargs): + if self.projection: + mapSrs = self.projection + layerSrs = self.getProj4String() + extent = None + overscan = 0 + if not hasattr(self, '_repeatLongitude'): + # If the original dataset is in a latitude/longitude + # projection, and does cover more than 360 degrees in longitude + # (with some slop), and is outside of the bounds of + # [-180, 180], we want to render it twice, once at the + # specified longitude and once offset to ensure that we cover + # [-180, 180]. This is done by altering the projection's + # prime meridian by 360 degrees. If none of the dataset is in + # the range of [-180, 180], this doesn't apply the shift + # either. + self._repeatLongitude = None + if self._proj4Proj(layerSrs).crs.is_geographic: + bounds = self.getBounds() + if bounds['xmax'] - bounds['xmin'] < 361: + if bounds['xmin'] < -180 and bounds['xmax'] > -180: + self._repeatLongitude = layerSrs + ' +pm=+360' + elif bounds['xmax'] > 180 and bounds['xmin'] < 180: + self._repeatLongitude = layerSrs + ' +pm=-360' + else: + mapSrs = '+proj=longlat +axis=enu' + layerSrs = '+proj=longlat +axis=enu' + # There appears to be a bug in some versions of mapnik/gdal when + # requesting a tile with a bounding box that has a corner exactly + # at (0, extentMaxY), so make a slightly larger image and crop it. + extent = '0 0 %d %d' % (self.sourceSizeX, self.sourceSizeY) + overscan = 1 + xmin, ymin, xmax, ymax = self.getTileCorners(z, x, y) + if self.projection: + # If we are using a projection, the tile could contain no data. + # Don't bother having mapnik render the blank tile -- just output + # it. + bounds = self.getBounds(self.projection) + if (xmin >= bounds['xmax'] or xmax <= bounds['xmin'] or + ymin >= bounds['ymax'] or ymax <= bounds['ymin']): + pilimg = PIL.Image.new('RGBA', (self.tileWidth, self.tileHeight)) + return self._outputTile( + pilimg, TILE_FORMAT_PIL, x, y, z, applyStyle=False, **kwargs) + if overscan: + pw = (xmax - xmin) / self.tileWidth + py = (ymax - ymin) / self.tileHeight + xmin, xmax = xmin - pw * overscan, xmax + pw * overscan + ymin, ymax = ymin - py * overscan, ymax + py * overscan + with self._getTileLock: + if not hasattr(self, '_mapnikMap'): + mapnik.logger.set_severity(mapnik.severity_type.Debug) + m = mapnik.Map( + self.tileWidth + overscan * 2, + self.tileHeight + overscan * 2, + mapSrs) + self.addStyle(m, layerSrs, extent) + if getattr(self, '_repeatLongitude', None): + self.addStyle(m, self._repeatLongitude, extent) + self._mapnikMap = m + else: + m = self._mapnikMap + m.zoom_to_box(mapnik.Box2d(xmin, ymin, xmax, ymax)) + img = mapnik.Image(self.tileWidth + overscan * 2, self.tileHeight + overscan * 2) + mapnik.render(m, img) + pilimg = PIL.Image.frombytes( + 'RGBA', (img.width(), img.height()), + getattr(img, 'tostring', getattr(img, 'to_string', None))()) + if overscan: + pilimg = pilimg.crop((1, 1, pilimg.width - overscan, pilimg.height - overscan)) + return self._outputTile(pilimg, TILE_FORMAT_PIL, x, y, z, applyStyle=False, **kwargs)
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return MapnikFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return MapnikFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_mapnik/girder_source.html b/_modules/large_image_source_mapnik/girder_source.html new file mode 100644 index 000000000..47d2e266b --- /dev/null +++ b/_modules/large_image_source_mapnik/girder_source.html @@ -0,0 +1,150 @@ + + + + + + large_image_source_mapnik.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_mapnik.girder_source

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+from large_image_source_gdal.girder_source import GDALGirderTileSource
+
+from . import MapnikFileTileSource
+
+
+
+[docs] +class MapnikGirderTileSource(MapnikFileTileSource, GDALGirderTileSource): + """ + Provides tile access to Girder items for mapnik layers. + """ + + name = 'mapnik' + cacheName = 'tilesource'
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_multi.html b/_modules/large_image_source_multi.html new file mode 100644 index 000000000..66bf53754 --- /dev/null +++ b/_modules/large_image_source_multi.html @@ -0,0 +1,1463 @@ + + + + + + large_image_source_multi — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_multi

+import builtins
+import copy
+import itertools
+import json
+import math
+import os
+import re
+import threading
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+from pathlib import Path
+
+import numpy as np
+import yaml
+
+import large_image
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource
+from large_image.tilesource.utilities import _makeSameChannelDepth, fullAlphaValue
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+jsonschema = None
+_validator = None
+
+
+def _lazyImport():
+    """
+    Import the jsonschema module.  This is done when needed rather than in the
+    module initialization because it is slow.
+    """
+    global jsonschema, _validator
+
+    if jsonschema is None:
+        try:
+            import jsonschema
+
+            _validator = jsonschema.Draft6Validator(MultiSourceSchema)
+        except ImportError:
+            msg = 'jsonschema module not found.'
+            raise TileSourceError(msg)
+
+
+SourceEntrySchema = {
+    'type': 'object',
+    'additionalProperties': True,
+    'properties': {
+        'name': {'type': 'string'},
+        'description': {'type': 'string'},
+        'path': {
+            'decription':
+                'The relative path, including file name if pathPattern is not '
+                'specified.  The relative path excluding file name if '
+                'pathPattern is specified.  Or, girder://id for Girder '
+                'sources.  If a specific tile source is specified that does '
+                'not need an actual path, the special value of `__none__` can '
+                'be used to bypass checking for an actual file.',
+            'type': 'string',
+        },
+        'pathPattern': {
+            'description':
+                'If specified, file names in the path are matched to this '
+                'regular expression, sorted in C-sort order.  This can '
+                'populate other properties via named expressions, e.g., '
+                'base_(?<xy>\\d+).png.  Add 1 to the name for 1-based '
+                'numerical values.',
+            'type': 'string',
+        },
+        'sourceName': {
+            'description':
+                'Require a specific source by name.  This is one of the '
+                'large_image source names (e.g., this one is "multi".',
+            'type': 'string',
+        },
+        # 'projection': {
+        #     'description':
+        #         'If specified, the source is treated as non-geospatial and '
+        #         'then a projection is added.  Set to None/null to use its '
+        #         'own projection if a overall projection was specified.',
+        #     'type': 'string',
+        # },
+        # corner points in the projection?
+        'frame': {
+            'description':
+                'Base value for all frames; only use this if the data does '
+                'not conceptually have z, t, xy, or c arrangement.',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        'z': {
+            'description': 'Base value for all frames',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        't': {
+            'description': 'Base value for all frames',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        'xy': {
+            'description': 'Base value for all frames',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        'c': {
+            'description': 'Base value for all frames',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        'zSet': {
+            'description': 'Override value for frame',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        'tSet': {
+            'description': 'Override value for frame',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        'xySet': {
+            'description': 'Override value for frame',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        'cSet': {
+            'description': 'Override value for frame',
+            'type': 'integer',
+            'minimum': 0,
+        },
+        'zValues': {
+            'description':
+                'The numerical z position of the different z indices of the '
+                'source.  If only one value is specified, other indices are '
+                'shifted based on the source.  If fewer values are given than '
+                'z indices, the last two value given imply a stride for the '
+                'remainder.',
+            'type': 'array',
+            'items': {'type': 'number'},
+            'minItems': 1,
+        },
+        'tValues': {
+            'description':
+                'The numerical t position of the different t indices of the '
+                'source.  If only one value is specified, other indices are '
+                'shifted based on the source.  If fewer values are given than '
+                't indices, the last two value given imply a stride for the '
+                'remainder.',
+            'type': 'array',
+            'items': {'type': 'number'},
+            'minItems': 1,
+        },
+        'xyValues': {
+            'description':
+                'The numerical xy position of the different xy indices of the '
+                'source.  If only one value is specified, other indices are '
+                'shifted based on the source.  If fewer values are given than '
+                'xy indices, the last two value given imply a stride for the '
+                'remainder.',
+            'type': 'array',
+            'items': {'type': 'number'},
+            'minItems': 1,
+        },
+        'cValues': {
+            'description':
+                'The numerical c position of the different c indices of the '
+                'source.  If only one value is specified, other indices are '
+                'shifted based on the source.  If fewer values are given than '
+                'c indices, the last two value given imply a stride for the '
+                'remainder.',
+            'type': 'array',
+            'items': {'type': 'number'},
+            'minItems': 1,
+        },
+        'frameValues': {
+            'description':
+                'The numerical frame position of the different frame indices '
+                'of the source.  If only one value is specified, other '
+                'indices are shifted based on the source.  If fewer values '
+                'are given than frame indices, the last two value given imply '
+                'a stride for the remainder.',
+            'type': 'array',
+            'items': {'type': 'number'},
+            'minItems': 1,
+        },
+        'channel': {
+            'description':
+                'A channel name to correspond with the main image.  Ignored '
+                'if c, cValues, or channels is specified.',
+            'type': 'string',
+        },
+        'channels': {
+            'description':
+                'A list of channel names used to correspond channels in this '
+                'source with the main image.  Ignored if c or cValues is '
+                'specified.',
+            'type': 'array',
+            'items': {'type': 'string'},
+            'minItems': 1,
+        },
+        'zStep': {
+            'description':
+                'Step value for multiple files included via pathPattern.  '
+                'Applies to z or zValues',
+            'type': 'integer',
+            'exclusiveMinimum': 0,
+        },
+        'tStep': {
+            'description':
+                'Step value for multiple files included via pathPattern.  '
+                'Applies to t or tValues',
+            'type': 'integer',
+            'exclusiveMinimum': 0,
+        },
+        'xyStep': {
+            'description':
+                'Step value for multiple files included via pathPattern.  '
+                'Applies to x or xyValues',
+            'type': 'integer',
+            'exclusiveMinimum': 0,
+        },
+        'xStep': {
+            'description':
+                'Step value for multiple files included via pathPattern.  '
+                'Applies to c or cValues',
+            'type': 'integer',
+            'exclusiveMinimum': 0,
+        },
+        'framesAsAxes': {
+            'description':
+                'An object with keys as axes and values as strides to '
+                'interpret the source frames.  This overrides the internal '
+                'metadata for frames.',
+            'type': 'object',
+            'patternProperties': {
+                '^(c|t|z|xy)$': {
+                    'type': 'integer',
+                    'exclusiveMinimum': 0,
+                },
+            },
+            'additionalProperties': False,
+        },
+        'position': {
+            'type': 'object',
+            'additionalProperties': False,
+            'description':
+                'The image can be translated with x, y offset, apply an '
+                'affine transform, and scaled.  If only part of the source is '
+                'desired, a crop can be applied before the transformation.',
+            'properties': {
+                'x': {'type': 'number'},
+                'y': {'type': 'number'},
+                'crop': {
+                    'description':
+                        'Crop the source before applying a '
+                        'position transform',
+                    'type': 'object',
+                    'additionalProperties': False,
+                    'properties': {
+                        'left': {'type': 'integer'},
+                        'top': {'type': 'integer'},
+                        'right': {'type': 'integer'},
+                        'bottom': {'type': 'integer'},
+                    },
+                    # TODO: Add polygon option
+                    # TODO: Add postTransform option
+                },
+                'scale': {
+                    'description':
+                        'Values less than 1 will downsample the source.  '
+                        'Values greater than 1 will upsample it.',
+                    'type': 'number',
+                    'exclusiveMinimum': 0,
+                },
+                's11': {'type': 'number'},
+                's12': {'type': 'number'},
+                's21': {'type': 'number'},
+                's22': {'type': 'number'},
+            },
+        },
+        'frames': {
+            'description': 'List of frames to use from source',
+            'type': 'array',
+            'items': {'type': 'integer'},
+        },
+        'sampleScale': {
+            'description':
+                'Each pixel sample values is divided by this scale after any '
+                'sampleOffset has been applied',
+            'type': 'number',
+        },
+        'sampleOffset': {
+            'description':
+                'This is added to each pixel sample value before any '
+                'sampleScale is applied',
+            'type': 'number',
+        },
+        'style': {'type': 'object'},
+        'params': {
+            'description':
+                'Additional parameters to pass to the base tile source',
+            'type': 'object',
+        },
+    },
+    'required': [
+        'path',
+    ],
+}
+
+MultiSourceSchema = {
+    '$schema': 'http://json-schema.org/schema#',
+    'type': 'object',
+    'additionalProperties': False,
+    'properties': {
+        'name': {'type': 'string'},
+        'description': {'type': 'string'},
+        'width': {'type': 'integer', 'exclusiveMinimum': 0},
+        'height': {'type': 'integer', 'exclusiveMinimum': 0},
+        'tileWidth': {'type': 'integer', 'exclusiveMinimum': 0},
+        'tileHeight': {'type': 'integer', 'exclusiveMinimum': 0},
+        'channels': {
+            'description': 'A list of channel names',
+            'type': 'array',
+            'items': {'type': 'string'},
+            'minItems': 1,
+        },
+        'scale': {
+            'type': 'object',
+            'additionalProperties': False,
+            'properties': {
+                'mm_x': {'type': 'number', 'exclusiveMinimum': 0},
+                'mm_y': {'type': 'number', 'exclusiveMinimum': 0},
+                'magnification': {'type': 'integer', 'exclusiveMinimum': 0},
+            },
+        },
+        # 'projection': {
+        #     'description': 'If specified, sources are treated as '
+        #                    'non-geospatial and then this projection is added',
+        #     'type': 'string',
+        # },
+        # corner points in the projection?
+        'backgroundColor': {
+            'description': 'A list of background color values (fill color) in '
+                           'the same scale and band order as the first tile '
+                           'source (e.g., white might be [255, 255, 255] for '
+                           'a three channel image).',
+            'type': 'array',
+            'items': {'type': 'number'},
+        },
+        'basePath': {
+            'decription':
+                'A relative path that is used as a base for all paths in '
+                'sources.  Defaults to the directory of the main file.',
+            'type': 'string',
+        },
+        'uniformSources': {
+            'description':
+                'If true and the first two sources are similar in frame '
+                'layout and size, assume all sources are so similar',
+            'type': 'boolean',
+        },
+        'dtype': {
+            'description': 'If present, a numpy dtype name to use for the data.',
+            'type': 'string',
+        },
+        'singleBand': {
+            'description':
+                'If true, output only the first band of compositied results',
+            'type': 'boolean',
+        },
+        'axes': {
+            'description': 'A list of additional axes that will be parsed.  '
+                           'The default axes are z, t, xy, and c.  It is '
+                           'recommended that additional axes use terse names '
+                           'and avoid x, y, and s.',
+            'type': 'array',
+            'items': {'type': 'string'},
+        },
+        'sources': {
+            'type': 'array',
+            'items': SourceEntrySchema,
+        },
+        # TODO: add merge method for cases where the are pixels from multiple
+        # sources in the same output location.
+    },
+    'required': [
+        'sources',
+    ],
+}
+
+
+
+[docs] +class MultiFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to a composite of other tile sources. + """ + + cacheName = 'tilesource' + name = 'multi' + extensions = { + None: SourcePriority.MEDIUM, + 'json': SourcePriority.PREFERRED, + 'yaml': SourcePriority.PREFERRED, + 'yml': SourcePriority.PREFERRED, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'application/json': SourcePriority.PREFERRED, + 'application/yaml': SourcePriority.PREFERRED, + } + + _minTileSize = 64 + _maxTileSize = 4096 + _defaultTileSize = 256 + _maxOpenHandles = 6 + + def __init__(self, path, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + _lazyImport() + self._validator = _validator + self._largeImagePath = self._getLargeImagePath() + self._lastOpenSourceLock = threading.RLock() + # 'c' must be first as channels are special because they can have names + self._axesList = ['c', 'z', 't', 'xy'] + if not os.path.isfile(self._largeImagePath): + try: + possibleYaml = self._largeImagePath.split('multi://', 1)[-1] + self._info = yaml.safe_load(possibleYaml) + self._validator.validate(self._info) + self._basePath = Path('.') + except Exception: + raise TileSourceFileNotFoundError(self._largeImagePath) from None + else: + try: + with builtins.open(self._largeImagePath) as fptr: + start = fptr.read(1024).strip() + if start[:1] not in ('{', '#', '-') and (start[:1] < 'a' or start[:1] > 'z'): + msg = 'File cannot be opened via multi-source reader.' + raise TileSourceError(msg) + fptr.seek(0) + try: + import orjson + self._info = orjson.loads(fptr.read()) + except Exception: + fptr.seek(0) + self._info = yaml.safe_load(fptr) + except (json.JSONDecodeError, yaml.YAMLError, UnicodeDecodeError): + msg = 'File cannot be opened via multi-source reader.' + raise TileSourceError(msg) + try: + self._validator.validate(self._info) + except jsonschema.ValidationError: + msg = 'File cannot be validated via multi-source reader.' + raise TileSourceError(msg) + self._basePath = Path(self._largeImagePath).parent + self._basePath /= Path(self._info.get('basePath', '.')) + for axis in self._info.get('axes', []): + if axis not in self._axesList: + self._axesList.append(axis) + self._collectFrames() + + def _resolvePathPatterns(self, sources, source): + """ + Given a source resolve pathPattern entries to specific paths. + Ensure that all paths exist. + + :param sources: a list to append found sources to. + :param source: the specific source record with a pathPattern to + resolve. + """ + kept = [] + pattern = re.compile(source['pathPattern']) + basedir = self._basePath / source['path'] + if (self._basePath.name == Path(self._largeImagePath).name and + (self._basePath.parent / source['path']).is_dir()): + basedir = self._basePath.parent / source['path'] + basedir = basedir.resolve() + for entry in basedir.iterdir(): + match = pattern.search(entry.name) + if match: + if entry.is_file(): + kept.append((entry.name, entry, match)) + elif entry.is_dir() and (entry / entry.name).is_file(): + kept.append((entry.name, entry / entry.name, match)) + for idx, (_, entry, match) in enumerate(sorted(kept)): + subsource = copy.deepcopy(source) + # Use named match groups to augment source values. + for k, v in match.groupdict().items(): + if v.isdigit(): + v = int(v) + if k.endswith('1'): + v -= 1 + if '.' in k: + subsource.setdefault(k.split('.', 1)[0], {})[k.split('.', 1)[1]] = v + else: + subsource[k] = v + subsource['path'] = entry + for axis in self._axesList: + stepKey = '%sStep' % axis + valuesKey = '%sValues' % axis + if stepKey in source: + if axis in source or valuesKey not in source: + subsource[axis] = subsource.get(axis, 0) + idx * source[stepKey] + else: + subsource[valuesKey] = [ + val + idx * source[stepKey] for val in subsource[valuesKey]] + del subsource['pathPattern'] + sources.append(subsource) + + def _resolveSourcePath(self, sources, source): + """ + Given a single source without a pathPattern, resolve to a specific + path, ensuring that it exists. + + :param sources: a list to append found sources to. + :param source: the specific source record to resolve. + """ + source = copy.deepcopy(source) + if source['path'] != '__none__': + sourcePath = Path(source['path']) + source['path'] = self._basePath / sourcePath + if not source['path'].is_file(): + altpath = self._basePath.parent / sourcePath / sourcePath.name + if altpath.is_file(): + source['path'] = altpath + if not source['path'].is_file(): + raise TileSourceFileNotFoundError(str(source['path'])) + sources.append(source) + + def _resolveFramePaths(self, sourceList): + """ + Given a list of sources, resolve path and pathPattern entries to + specific paths. + Ensure that all paths exist. + + :param sourceList: a list of source entries to resolve and check. + :returns: sourceList: a expanded and checked list of sources. + """ + # we want to work with both _basePath / <path> and + # _basePath / .. / <path> / <name> to be compatible with Girder + # resource layouts. + sources = [] + for source in sourceList: + if source.get('pathPattern'): + self._resolvePathPatterns(sources, source) + else: + self._resolveSourcePath(sources, source) + for source in sources: + if hasattr(source.get('path'), 'resolve'): + source['path'] = source['path'].resolve(False) + return sources + + def _sourceBoundingBox(self, source, width, height): + """ + Given a source with a possible transform and an image width and height, + compute the bounding box for the source. If a crop is used, it is + included in the results. If a non-identify transform is used, both it + and its inverse are included in the results. + + :param source: a dictionary that may have a position record. + :param width: the width of the source to transform. + :param height: the height of the source to transform. + :returns: a dictionary with left, top, right, bottom of the bounding + box in the final coordinate space. + """ + pos = source.get('position') + bbox = {'left': 0, 'top': 0, 'right': width, 'bottom': height} + if not pos: + return bbox + x0, y0, x1, y1 = 0, 0, width, height + if 'crop' in pos: + x0 = min(max(pos['crop'].get('left', x0), 0), width) + y0 = min(max(pos['crop'].get('top', y0), 0), height) + x1 = min(max(pos['crop'].get('right', x1), x0), width) + y1 = min(max(pos['crop'].get('bottom', y1), y0), height) + bbox['crop'] = {'left': x0, 'top': y0, 'right': x1, 'bottom': y1} + corners = np.array([[x0, y0, 1], [x1, y0, 1], [x0, y1, 1], [x1, y1, 1]]) + m = np.identity(3) + m[0][0] = pos.get('s11', 1) * pos.get('scale', 1) + m[0][1] = pos.get('s12', 0) * pos.get('scale', 1) + m[0][2] = pos.get('x', 0) + m[1][0] = pos.get('s21', 0) * pos.get('scale', 1) + m[1][1] = pos.get('s22', 1) * pos.get('scale', 1) + m[1][2] = pos.get('y', 0) + if not np.array_equal(m, np.identity(3)): + bbox['transform'] = m + try: + bbox['inverse'] = np.linalg.inv(m) + except np.linalg.LinAlgError: + msg = 'The position for a source is not invertable (%r)' + raise TileSourceError(msg, pos) + transcorners = np.dot(m, corners.T) + bbox['left'] = min(transcorners[0]) + bbox['top'] = min(transcorners[1]) + bbox['right'] = max(transcorners[0]) + bbox['bottom'] = max(transcorners[1]) + return bbox + + def _axisKey(self, source, value, key): + """ + Get the value for a particular axis given the source specification. + + :param source: a source specification. + :param value: a default or initial value. + :param key: the axis key. One of frame, c, z, t, xy. + :returns: the axis key (an integer). + """ + if source.get('%sSet' % key) is not None: + return source.get('%sSet' % key) + vals = source.get('%sValues' % key) or [] + if not vals: + axisKey = value + source.get(key, 0) + elif len(vals) == 1: + axisKey = vals[0] + value + source.get(key, 0) + elif value < len(vals): + axisKey = vals[value] + source.get(key, 0) + else: + axisKey = (vals[len(vals) - 1] + (vals[len(vals) - 1] - vals[len(vals) - 2]) * + (value - len(vals) + source.get(key, 0))) + return axisKey + + def _adjustFramesAsAxes(self, frames, idx, framesAsAxes): + """ + Given a dictionary of axes and strides, relabel the indices in a frame + as if it was based on those strides. + + :param frames: a list of frames from the tile source. + :param idx: 0-based index of the frame to adjust. + :param framesAsAxes: dictionary of axes and strides to apply. + :returns: the adjusted frame record. + """ + axisRange = {} + slen = len(frames) + check = 1 + for stride, axis in sorted([[v, k] for k, v in framesAsAxes.items()], reverse=True): + axisRange[axis] = slen // stride + slen = stride + check *= axisRange[axis] + if check != len(frames) and not hasattr(self, '_warnedAdjustFramesAsAxes'): + self.logger.warning('framesAsAxes strides do not use all frames.') + self._warnedAdjustFramesAsAxes = True + frame = frames[idx].copy() + for axis in self._axesList: + frame.pop('Index' + axis.upper(), None) + for axis, stride in framesAsAxes.items(): + frame['Index' + axis.upper()] = (idx // stride) % axisRange[axis] + return frame + + def _addSourceToFrames(self, tsMeta, source, sourceIdx, frameDict): + """ + Add a source to the all appropriate frames. + + :param tsMeta: metadata from the source or from a matching uniform + source. + :param source: the source record. + :param sourceIdx: the index of the source. + :param frameDict: a dictionary to log the found frames. + """ + frames = tsMeta.get('frames', [{'Frame': 0, 'Index': 0}]) + # Channel names + channels = tsMeta.get('channels', []) + if source.get('channels'): + channels[:len(source['channels'])] = source['channels'] + elif source.get('channel'): + channels[:1] = [source['channel']] + if len(channels) > len(self._channels): + self._channels += channels[len(self._channels):] + if not any(key in source for key in { + 'frame', 'frameValues'} | + set(self._axesList) | + {f'{axis}Values' for axis in self._axesList}): + source = source.copy() + if len(frameDict['byFrame']): + source['frame'] = max(frameDict['byFrame'].keys()) + 1 + if len(frameDict['byAxes']): + source['z'] = max( + aKey[self._axesList.index('z')] for aKey in frameDict['byAxes']) + 1 + for frameIdx, frame in enumerate(frames): + if 'frames' in source and frameIdx not in source['frames']: + continue + if source.get('framesAsAxes'): + frame = self._adjustFramesAsAxes(frames, frameIdx, source.get('framesAsAxes')) + fKey = self._axisKey(source, frameIdx, 'frame') + cIdx = frame.get('IndexC', 0) + aKey = tuple(self._axisKey(source, frame.get(f'Index{axis.upper()}') or 0, axis) + for axis in self._axesList) + channel = channels[cIdx] if cIdx < len(channels) else None + # We add the channel name to our channel list if the individual + # source lists the channel name or a set of channels OR the source + # appends channels to an existing set. + if channel and channel not in self._channels and ( + 'channel' in source or 'channels' in source or + len(self._channels) == aKey[0]): + self._channels.append(channel) + # Adjust the channel number if the source named the channel; do not + # do so in other cases + if (channel and channel in self._channels and + 'c' not in source and 'cValues' not in source): + aKey = tuple([self._channels.index(channel)] + list(aKey[1:])) + kwargs = source.get('params', {}).copy() + if 'style' in source: + kwargs['style'] = source['style'] + kwargs.pop('frame', None) + kwargs.pop('encoding', None) + frameDict['byFrame'].setdefault(fKey, []) + frameDict['byFrame'][fKey].append({ + 'sourcenum': sourceIdx, + 'frame': frameIdx, + 'kwargs': kwargs, + }) + frameDict['axesAllowed'] = (frameDict['axesAllowed'] and ( + len(frames) <= 1 or 'IndexRange' in tsMeta)) or aKey != tuple([0] * len(aKey)) + frameDict['byAxes'].setdefault(aKey, []) + frameDict['byAxes'][aKey].append({ + 'sourcenum': sourceIdx, + 'frame': frameIdx, + 'kwargs': kwargs, + }) + + def _frameDictToFrames(self, frameDict): + """ + Given a frame dictionary, populate a frame list. + + :param frameDict: a dictionary with known frames stored in byAxes if + axesAllowed is True or byFrame if it is False. + :returns: a list of frames with enough information to generate them. + """ + frames = [] + if not frameDict['axesAllowed']: + frameCount = max(frameDict['byFrame']) + 1 + for frameIdx in range(frameCount): + frame = {'sources': frameDict['byFrame'].get(frameIdx, [])} + frames.append(frame) + else: + axesCount = [max(aKey[idx] for aKey in frameDict['byAxes']) + 1 + for idx in range(len(self._axesList))] + for aKey in itertools.product(*[range(count) for count in axesCount][::-1]): + aKey = tuple(aKey[::-1]) + frame = { + 'sources': frameDict['byAxes'].get(aKey, []), + } + for idx, axis in enumerate(self._axesList): + if axesCount[idx] > 1: + frame[f'Index{axis.upper()}'] = aKey[idx] + frames.append(frame) + return frames + + def _collectFrames(self): + """ + Using the specification in _info, enumerate the source files and open + at least the first two of them to build up the frame specifications. + """ + self._sources = sources = self._resolveFramePaths(self._info['sources']) + self.logger.debug('Sources: %r', sources) + + frameDict = {'byFrame': {}, 'byAxes': {}, 'axesAllowed': True} + numChecked = 0 + + self._associatedImages = {} + self._sourcePaths = {} + self._channels = self._info.get('channels') or [] + + absLargeImagePath = os.path.abspath(self._largeImagePath) + computedWidth = computedHeight = 0 + self.tileWidth = self._info.get('tileWidth') + self.tileHeight = self._info.get('tileHeight') + self._nativeMagnification = { + 'mm_x': self._info.get('scale', {}).get('mm_x') or None, + 'mm_y': self._info.get('scale', {}).get('mm_y') or None, + 'magnification': self._info.get('scale', {}).get('magnification') or None, + } + # Walk through the sources, opening at least the first two, and + # construct a frame list. Each frame is a list of sources that affect + # it along with the frame number from that source. + lastSource = None + bandCount = 0 + for sourceIdx, source in enumerate(sources): + path = source['path'] + if os.path.abspath(path) == absLargeImagePath: + msg = 'Multi source specification is self-referential' + raise TileSourceError(msg) + similar = False + if (lastSource and source['path'] == lastSource['path'] and + source.get('params') == lastSource.get('params')): + similar = True + if not similar and (numChecked < 2 or not self._info.get('uniformSources')): + # need kwargs of frame, style? + ts = self._openSource(source) + self.tileWidth = self.tileWidth or ts.tileWidth + self.tileHeight = self.tileHeight or ts.tileHeight + if not hasattr(self, '_firstdtype'): + self._firstdtype = ( + ts.dtype if not self._info.get('dtype') else + np.dtype(self._info['dtype'])) + if self._info.get('dtype'): + self._dtype = np.dtype(self._info['dtype']) + if not numChecked: + tsMag = ts.getNativeMagnification() + for key in self._nativeMagnification: + self._nativeMagnification[key] = ( + self._nativeMagnification[key] or tsMag.get(key)) + numChecked += 1 + tsMeta = ts.getMetadata() + bandCount = max(bandCount, ts.bandCount or 0) + if 'bands' in tsMeta and self._info.get('singleBand') is not True: + if not hasattr(self, '_bands'): + self._bands = {} + self._bands.update(tsMeta['bands']) + lastSource = source + self._bandCount = 1 if self._info.get('singleBand') else ( + len(self._bands) if hasattr(self, '_bands') else (bandCount or None)) + bbox = self._sourceBoundingBox(source, tsMeta['sizeX'], tsMeta['sizeY']) + computedWidth = max(computedWidth, int(math.ceil(bbox['right']))) + computedHeight = max(computedHeight, int(math.ceil(bbox['bottom']))) + # Record this path + if path not in self._sourcePaths: + self._sourcePaths[path] = { + 'frames': set(), + 'sourcenum': set(), + } + # collect associated images + for basekey in ts.getAssociatedImagesList(): + key = basekey + keyidx = 0 + while key in self._associatedImages: + keyidx += 1 + key = '%s-%d' % (basekey, keyidx) + self._associatedImages[key] = { + 'sourcenum': sourceIdx, + 'key': key, + } + source['metadata'] = tsMeta + source['bbox'] = bbox + self._sourcePaths[path]['sourcenum'].add(sourceIdx) + # process metadata to determine what frames are used, etc. + self._addSourceToFrames(tsMeta, source, sourceIdx, frameDict) + # Check frameDict and create frame record + self._frames = self._frameDictToFrames(frameDict) + self.tileWidth = min(max(self.tileWidth, self._minTileSize), self._maxTileSize) + self.tileHeight = min(max(self.tileHeight, self._minTileSize), self._maxTileSize) + self.sizeX = self._info.get('width') or computedWidth + self.sizeY = self._info.get('height') or computedHeight + self.levels = int(max(1, math.ceil(math.log( + max(self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1)) + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + return self._nativeMagnification.copy()
+ + + def _openSource(self, source, params=None): + """ + Open a tile source, possibly using a specific source. + + :param source: a dictionary with path, params, and possibly sourceName. + :param params: a dictionary of parameters to pass to the open call. + :returns: a tile source. + """ + with self._lastOpenSourceLock: + if (hasattr(self, '_lastOpenSource') and + self._lastOpenSource['source'] == source and + (self._lastOpenSource['params'] == params or ( + params == {} and self._lastOpenSource['params'] is None))): + return self._lastOpenSource['ts'] + if not len(large_image.tilesource.AvailableTileSources): + large_image.tilesource.loadTileSources() + if ('sourceName' not in source or + source['sourceName'] not in large_image.tilesource.AvailableTileSources): + openFunc = large_image.open + else: + openFunc = large_image.tilesource.AvailableTileSources[source['sourceName']] + origParams = params + if params is None: + params = source.get('params', {}) + ts = openFunc(source['path'], **params) + if (self._dtype and np.dtype(ts.dtype).kind == 'f' and self._dtype.kind != 'f' and + 'sampleScale' not in source and 'sampleOffset' not in source): + minval = maxval = 0 + for f in range(ts.frames): + ftile = ts.getTile(x=0, y=0, z=0, frame=f, numpyAllowed='always') + minval = min(minval, np.amin(ftile)) + maxval = max(maxval, np.amax(ftile)) + if minval >= 0 and maxval <= 1: + source['sampleScale'] = None + elif minval >= 0: + source['sampleScale'] = 2 ** math.ceil(math.log2(maxval)) + else: + source['sampleScale'] = 2 ** math.ceil(math.log2(max(-minval, maxval)) + 1) + source['sampleOffset'] = source['sampleScale'] / 2 + source['sourceName'] = ts.name + with self._lastOpenSourceLock: + self._lastOpenSource = { + 'source': source, + 'params': origParams, + 'ts': ts, + } + return ts + +
+[docs] + def getAssociatedImage(self, imageKey, *args, **kwargs): + """ + Return an associated image. + + :param imageKey: the key of the associated image to retrieve. + :param kwargs: optional arguments. Some options are width, height, + encoding, jpegQuality, jpegSubsampling, and tiffCompression. + :returns: imageData, imageMime: the image data and the mime type, or + None if the associated image doesn't exist. + """ + if imageKey not in self._associatedImages: + return + source = self._sources[self._associatedImages[imageKey]['sourcenum']] + ts = self._openSource(source) + return ts.getAssociatedImage(self._associatedImages[imageKey]['key'], *args, **kwargs)
+ + +
+[docs] + def getAssociatedImagesList(self): + """ + Return a list of associated images. + + :return: the list of image keys. + """ + return sorted(self._associatedImages.keys())
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + result = super().getMetadata() + if len(self._frames) > 1: + result['frames'] = [ + {k: v for k, v in frame.items() if k.startswith('Index')} + for frame in self._frames] + self._addMetadataFrameInformation(result, self._channels) + if hasattr(self, '_bands'): + result['bands'] = self._bands.copy() + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. Also, only the first 100 sources are used. + + :returns: a dictionary of data or None. + """ + result = { + 'frames': copy.deepcopy(self._frames), + 'sources': copy.deepcopy(self._sources), + 'sourceFiles': [], + } + for path in list(self._sourcePaths.values())[:100]: + source = self._sources[min(path['sourcenum'])] + ts = self._openSource(source) + result['sourceFiles'].append({ + 'path': source['path'], + 'internal': ts.getInternalMetadata(), + }) + return result
+ + + def _mergeTiles(self, base, tile, x, y): + """ + Add a tile to an existing tile. The existing tile is expanded as + needed, and the number of channels will always be the greater of the + two. + + :param base: numpy array base tile. May be None. May be modified. + :param tile: numpy tile to add. + :param x: location to add the tile. + :param y: location to add the tile. + :returns: a numpy tile. + """ + # Replace non blank pixels, aggregating opacity appropriately + x = int(round(x)) + y = int(round(y)) + if base is None and not x and not y: + return tile + if base is None: + base = np.zeros((0, 0, tile.shape[2]), dtype=tile.dtype) + base, tile = _makeSameChannelDepth(base, tile) + if base.shape[0] < tile.shape[0] + y: + vfill = np.zeros( + (tile.shape[0] + y - base.shape[0], base.shape[1], base.shape[2]), + dtype=base.dtype) + if base.shape[2] in {2, 4}: + vfill[:, :, -1] = fullAlphaValue(base.dtype) + base = np.vstack((base, vfill)) + if base.shape[1] < tile.shape[1] + x: + hfill = np.zeros( + (base.shape[0], tile.shape[1] + x - base.shape[1], base.shape[2]), + dtype=base.dtype) + if base.shape[2] in {2, 4}: + hfill[:, :, -1] = fullAlphaValue(base.dtype) + base = np.hstack((base, hfill)) + if base.flags.writeable is False: + base = base.copy() + if base.shape[2] in {2, 4}: + baseA = base[y:y + tile.shape[0], x:x + tile.shape[1], -1].astype( + float) / fullAlphaValue(base.dtype) + tileA = tile[:, :, -1].astype(float) / fullAlphaValue(tile.dtype) + outA = tileA + baseA * (1 - tileA) + base[y:y + tile.shape[0], x:x + tile.shape[1], :-1] = ( + np.where(tileA[..., np.newaxis], tile[:, :, :-1] * tileA[..., np.newaxis], 0) + + base[y:y + tile.shape[0], x:x + tile.shape[1], :-1] * baseA[..., np.newaxis] * + (1 - tileA[..., np.newaxis]) + ) / np.where(outA[..., np.newaxis], outA[..., np.newaxis], 1) + base[y:y + tile.shape[0], x:x + tile.shape[1], -1] = outA * fullAlphaValue(base.dtype) + else: + base[y:y + tile.shape[0], x:x + tile.shape[1], :] = tile + return base + + def _getTransformedTile(self, ts, transform, corners, scale, frame, crop=None): + """ + Determine where the target tile's corners are located on the source. + Fetch that so that we have at least sqrt(2) more resolution, then use + scikit-image warp to transform it. scikit-image does a better and + faster job than scipy.ndimage.affine_transform. + + :param ts: the source of the image to transform. + :param transform: a 3x3 affine 2d matrix for transforming the source + image at full resolution to the target tile at full resolution. + :param corners: corners of the destination in full res coordinates. + corner 0 must be the upper left, 2 must be the lower right. + :param scale: scaling factor from full res to the target resolution. + :param frame: frame number of the source image. + :param crop: an optional dictionary to crop the source image in full + resolution, untransformed coordinates. This may contain left, top, + right, and bottom values in pixels. + :returns: a numpy array tile or None, x, y coordinates within the + target tile for the placement of the numpy tile array. + """ + try: + import skimage.transform + except ImportError: + msg = 'scikit-image is required for affine transforms.' + raise TileSourceError(msg) + # From full res source to full res destination + transform = transform.copy() if transform is not None else np.identity(3) + # Scale dest corners to actual size; adjust transform for the same + corners = np.array(corners) + corners[:, :2] //= scale + transform[:2, :] /= scale + # Offset so our target is the actual destination array we use + transform[0][2] -= corners[0][0] + transform[1][2] -= corners[0][1] + corners[:, :2] -= corners[0, :2] + outw, outh = corners[2][0], corners[2][1] + if not outh or not outw: + return None, 0, 0 + srccorners = np.dot(np.linalg.inv(transform), np.array(corners).T).T.tolist() + minx = min(c[0] for c in srccorners) + maxx = max(c[0] for c in srccorners) + miny = min(c[1] for c in srccorners) + maxy = max(c[1] for c in srccorners) + srcscale = max((maxx - minx) / outw, (maxy - miny) / outh) + # we only need every 1/srcscale pixel. + srcscale = int(2 ** math.log2(max(1, srcscale))) + # Pad to reduce edge effects at tile boundaries + border = int(math.ceil(2 * srcscale)) + region = { + 'left': int(max(0, minx - border) // srcscale) * srcscale, + 'top': int(max(0, miny - border) // srcscale) * srcscale, + 'right': int((min(ts.sizeX, maxx + border) + srcscale - 1) // srcscale) * srcscale, + 'bottom': int((min(ts.sizeY, maxy + border) + srcscale - 1) // srcscale) * srcscale, + } + if crop: + region['left'] = max(region['left'], crop.get('left', region['left'])) + region['top'] = max(region['top'], crop.get('top', region['top'])) + region['right'] = min(region['right'], crop.get('right', region['right'])) + region['bottom'] = min(region['bottom'], crop.get('bottom', region['bottom'])) + output = { + 'maxWidth': (region['right'] - region['left']) // srcscale, + 'maxHeight': (region['bottom'] - region['top']) // srcscale, + } + if output['maxWidth'] <= 0 or output['maxHeight'] <= 0: + return None, 0, 0 + srcImage, _ = ts.getRegion( + region=region, output=output, frame=frame, resample=None, + format=TILE_FORMAT_NUMPY) + # This is the region we actually took in our source coordinates, scaled + # for if we took a low res version + regioncorners = np.array([ + [region['left'] // srcscale, region['top'] // srcscale, 1], + [region['right'] // srcscale, region['top'] // srcscale, 1], + [region['right'] // srcscale, region['bottom'] // srcscale, 1], + [region['left'] // srcscale, region['bottom'] // srcscale, 1]], dtype=float) + # adjust our transform if we took a low res version of the source + transform[:2, :2] *= srcscale + # Find where the source corners land on the destination. + preshiftcorners = (np.dot(transform, regioncorners.T).T).tolist() + regioncorners[:, :2] -= regioncorners[0, :2] + destcorners = (np.dot(transform, regioncorners.T).T).tolist() + offsetx, offsety = None, None + for idx in range(4): + if offsetx is None or destcorners[idx][0] < offsetx: + x = preshiftcorners[idx][0] + offsetx = destcorners[idx][0] - (x - math.floor(x)) + if offsety is None or destcorners[idx][1] < offsety: + y = preshiftcorners[idx][1] + offsety = destcorners[idx][1] - (y - math.floor(y)) + transform[0][2] -= offsetx + transform[1][2] -= offsety + x, y = int(math.floor(x)), int(math.floor(y)) + # Recompute where the source corners will land + destcorners = (np.dot(transform, regioncorners.T).T).tolist() + destShape = [ + max(max(math.ceil(c[1]) for c in destcorners), srcImage.shape[0]), + max(max(math.ceil(c[0]) for c in destcorners), srcImage.shape[1]), + ] + if max(0, -x) or max(0, -y): + transform[0][2] -= max(0, -x) + transform[1][2] -= max(0, -y) + destShape[0] -= max(0, -y) + destShape[1] -= max(0, -x) + x += max(0, -x) + y += max(0, -y) + destShape = [min(destShape[0], outh - y), min(destShape[1], outw - x)] + if destShape[0] <= 0 or destShape[1] <= 0: + return None, None, None + # Add an alpha band if needed + if srcImage.shape[2] in {1, 3}: + _, srcImage = _makeSameChannelDepth(np.zeros((1, 1, srcImage.shape[2] + 1)), srcImage) + # skimage.transform.warp is faster and has less artifacts than + # scipy.ndimage.affine_transform. It is faster than using cupy's + # version of scipy's affine_transform when the source and destination + # images are converted from numpy to cupy and back in this method. + destImage = skimage.transform.warp( + # Although using np.float32 could reduce memory use, it doesn't + # provide any speed improvement + srcImage.astype(float), + skimage.transform.AffineTransform(np.linalg.inv(transform)), + order=3, + output_shape=(destShape[0], destShape[1], srcImage.shape[2]), + ).astype(srcImage.dtype) + return destImage, x, y + + def _addSourceToTile(self, tile, sourceEntry, corners, scale): + """ + Add a source to the current tile. + + :param tile: a numpy array with the tile, or None if there is no data + yet. + :param sourceEntry: the current record from the sourceList. This + contains the sourcenum, kwargs to apply when opening the source, + and the frame within the source to fetch. + :param corners: the four corners of the tile in the main image space + coordinates. + :param scale: power of 2 scale of the output; this is the number of + pixels that are conceptually aggregated from the source for one + output pixel. + :returns: a numpy array of the tile. + """ + source = self._sources[sourceEntry['sourcenum']] + # If tile is outside of bounding box, skip it + bbox = source['bbox'] + if (corners[2][0] <= bbox['left'] or corners[0][0] >= bbox['right'] or + corners[2][1] <= bbox['top'] or corners[0][1] >= bbox['bottom']): + return tile + ts = self._openSource(source, sourceEntry['kwargs']) + transform = bbox.get('transform') + x = y = 0 + # If there is no transform or the diagonals are positive and there is + # no sheer and integer pixel alignment, use getRegion with an + # appropriate size + scaleX = transform[0][0] if transform is not None else 1 + scaleY = transform[1][1] if transform is not None else 1 + if ((transform is None or ( + transform[0][0] > 0 and transform[0][1] == 0 and + transform[1][0] == 0 and transform[1][1] > 0 and + transform[0][2] % scaleX == 0 and transform[1][2] % scaleY == 0)) and + ((scaleX % scale) == 0 or math.log(scaleX, 2).is_integer()) and + ((scaleY % scale) == 0 or math.log(scaleY, 2).is_integer())): + srccorners = ( + list(np.dot(bbox['inverse'], np.array(corners).T).T) + if transform is not None else corners) + region = { + 'left': srccorners[0][0], 'top': srccorners[0][1], + 'right': srccorners[2][0], 'bottom': srccorners[2][1], + } + output = { + 'maxWidth': (corners[2][0] - corners[0][0]) // scale, + 'maxHeight': (corners[2][1] - corners[0][1]) // scale, + } + if region['left'] < 0: + x -= region['left'] * scaleX // scale + output['maxWidth'] += int(region['left'] * scaleX // scale) + region['left'] = 0 + if region['top'] < 0: + y -= region['top'] * scaleY // scale + output['maxHeight'] += int(region['top'] * scaleY // scale) + region['top'] = 0 + if region['right'] > source['metadata']['sizeX']: + output['maxWidth'] -= int( + (region['right'] - source['metadata']['sizeX']) * scaleX // scale) + region['right'] = source['metadata']['sizeX'] + if region['bottom'] > source['metadata']['sizeY']: + output['maxHeight'] -= int( + (region['bottom'] - source['metadata']['sizeY']) * scaleY // scale) + region['bottom'] = source['metadata']['sizeY'] + for key in region: + region[key] = int(round(region[key])) + self.logger.debug('getRegion: ts: %r, region: %r, output: %r', ts, region, output) + sourceTile, _ = ts.getRegion( + region=region, output=output, frame=sourceEntry.get('frame', 0), + resample=None, format=TILE_FORMAT_NUMPY) + else: + sourceTile, x, y = self._getTransformedTile( + ts, transform, corners, scale, sourceEntry.get('frame', 0), + source.get('position', {}).get('crop')) + if sourceTile is not None and all(dim > 0 for dim in sourceTile.shape): + targetDtype = np.dtype(self._info.get('dtype', ts.dtype)) + changeDtype = sourceTile.dtype != targetDtype + if source.get('sampleScale') or source.get('sampleOffset'): + sourceTile = sourceTile.astype(float) + if source.get('sampleOffset'): + sourceTile[:, :, :-1] += source['sampleOffset'] + if source.get('sampleScale') and source.get('sampleScale') != 1: + sourceTile[:, :, :-1] /= source['sampleScale'] + if sourceTile.dtype != targetDtype: + if changeDtype: + sourceTile = ( + sourceTile.astype(float) * fullAlphaValue(targetDtype) / + fullAlphaValue(sourceTile)) + sourceTile = sourceTile.astype(targetDtype) + tile = self._mergeTiles(tile, sourceTile, x, y) + return tile + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame, len(self._frames) if hasattr(self, '_frames') else None) + scale = 2 ** (self.levels - 1 - z) + corners = [[ + x * self.tileWidth * scale, + y * self.tileHeight * scale, + 1, + ], [ + min((x + 1) * self.tileWidth * scale, self.sizeX), + y * self.tileHeight * scale, + 1, + ], [ + min((x + 1) * self.tileWidth * scale, self.sizeX), + min((y + 1) * self.tileHeight * scale, self.sizeY), + 1, + ], [ + x * self.tileWidth * scale, + min((y + 1) * self.tileHeight * scale, self.sizeY), + 1, + ]] + sourceList = self._frames[frame]['sources'] + tile = None + # If the first source does not completely cover the output tile or uses + # a transformation, create a tile that is the desired size and fill it + # with the background color. + fill = not len(sourceList) + if not fill: + firstsource = self._sources[sourceList[0]['sourcenum']] + fill = 'transform' in firstsource['bbox'] or any( + cx < firstsource['bbox']['left'] or + cx > firstsource['bbox']['right'] or + cy < firstsource['bbox']['top'] or + cy > firstsource['bbox']['bottom'] for cx, cy, _ in corners) + if fill: + colors = self._info.get('backgroundColor') + if colors: + tile = np.full((self.tileHeight, self.tileWidth, len(colors)), + colors, + dtype=getattr(self, '_firstdtype', np.uint8)) + # Add each source to the tile + for sourceEntry in sourceList: + tile = self._addSourceToTile(tile, sourceEntry, corners, scale) + if tile is None: + # TODO number of channels? + colors = self._info.get('backgroundColor', [0]) + if colors: + tile = np.full((self.tileHeight, self.tileWidth, len(colors)), + colors, + dtype=getattr(self, '_firstdtype', np.uint8)) + if self._info.get('singleBand'): + tile = tile[:, :, :1] + elif tile.shape[2] in {2, 4} and (self._bandCount or tile.shape[2]) < tile.shape[2]: + # remove a needless alpha channel + if np.all(tile[:, :, -1] == fullAlphaValue(tile)): + tile = tile[:, :, :-1] + if self._bandCount and tile.shape[2] < self._bandCount: + _, tile = _makeSameChannelDepth(np.zeros((1, 1, self._bandCount)), tile) + # We should always have a tile + return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return MultiFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return MultiFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_multi/girder_source.html b/_modules/large_image_source_multi/girder_source.html new file mode 100644 index 000000000..d7792c3f7 --- /dev/null +++ b/_modules/large_image_source_multi/girder_source.html @@ -0,0 +1,163 @@ + + + + + + large_image_source_multi.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_multi.girder_source

+import copy
+
+from girder_large_image.girder_tilesource import GirderTileSource
+from girder_large_image.models.image_item import ImageItem
+
+from large_image.exceptions import TileSourceFileNotFoundError
+
+from . import MultiFileTileSource
+
+
+
+[docs] +class MultiGirderTileSource(MultiFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with files that the multi source can + read. + """ + + cacheName = 'tilesource' + name = 'multi' + + _mayHaveAdjacentFiles = True + + def _resolveSourcePath(self, sources, source): + try: + super()._resolveSourcePath(sources, source) + except TileSourceFileNotFoundError: + prefix = 'girder://' + potentialId = source['path'] + if potentialId.startswith(prefix): + potentialId = potentialId[len(prefix):] + if '://' not in potentialId: + try: + item = ImageItem().load(potentialId, force=True) + ts = ImageItem().tileSource(item) + source = copy.deepcopy(source) + source['path'] = ts._getLargeImagePath() + source['sourceName'] = item['largeImage']['sourceName'] + sources.append(source) + return + except Exception: + pass + raise
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_nd2.html b/_modules/large_image_source_nd2.html new file mode 100644 index 000000000..4cdecd204 --- /dev/null +++ b/_modules/large_image_source_nd2.html @@ -0,0 +1,483 @@ + + + + + + large_image_source_nd2 — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_nd2

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import math
+import os
+import threading
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import numpy as np
+
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource
+
+nd2 = None
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+def _lazyImport():
+    """
+    Import the nd2 module.  This is done when needed rather than in the module
+    initialization because it is slow.
+    """
+    global nd2
+
+    if nd2 is None:
+        try:
+            import nd2
+        except ImportError:
+            msg = 'nd2 module not found.'
+            raise TileSourceError(msg)
+
+
+
+[docs] +def namedtupleToDict(obj): + """ + Convert a namedtuple to a plain dictionary. + + :param obj: the object to convert + :returns: a dictionary or the original object. + """ + if hasattr(obj, '__dict__') and not isinstance(obj, dict): + obj = obj.__dict__ + if isinstance(obj, dict): + obj = { + k: namedtupleToDict(v) for k, v in obj.items() + if not k.startswith('_')} + return {k: v for k, v in obj.items() if v is not None and v != {} and v != ''} + if isinstance(obj, (tuple, list)): + obj = [namedtupleToDict(v) for v in obj] + if not any(v is not None and v != {} and v != '' for v in obj): + return None + return obj
+ + + +
+[docs] +def diffObj(obj1, obj2): + """ + Given two objects, report the differences that exist in the first object + that are not in the second object. + + :param obj1: the first object to compare. Only values present in this + object are returned. + :param obj2: the second object to compare. + :returns: a subset of obj1. + """ + if obj1 == obj2: + return None + if not isinstance(obj1, type(obj2)): + return obj1 + if isinstance(obj1, (list, tuple)): + return [diffObj(obj1[idx], obj2[idx]) for idx in range(len(obj1))] + if isinstance(obj1, dict): + diff = {k: diffObj(v, obj2.get(k)) for k, v in obj1.items()} + diff = {k: v for k, v in diff.items() if v is not None} + return diff + return obj1
+ + + +
+[docs] +class ND2FileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to nd2 files the nd2 library can read. + """ + + cacheName = 'tilesource' + name = 'nd2' + extensions = { + None: SourcePriority.LOW, + 'nd2': SourcePriority.PREFERRED, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'image/nd2': SourcePriority.PREFERRED, + } + + # If frames are smaller than this they are served as single tiles, which + # can be more efficient than handling multiple tiles. + _singleTileThreshold = 2048 + _tileSize = 512 + + def __init__(self, path, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + self._largeImagePath = str(self._getLargeImagePath()) + + _lazyImport() + try: + self._nd2 = nd2.ND2File(self._largeImagePath, validate_frames=True) + except Exception: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + msg = 'File cannot be opened via the nd2 source.' + raise TileSourceError(msg) + # We use dask to allow lazy reading of large images + try: + self._nd2array = self._nd2.to_dask(copy=False, wrapper=False) + except (TypeError, ValueError) as exc: + self.logger.debug('Failed to read nd2 file: %s', exc) + msg = 'File cannot be opened via the nd2 source.' + raise TileSourceError(msg) + arrayOrder = list(self._nd2.sizes) + # Reorder this so that it is XY (P), T, Z, C, Y, X, S (or at least end + # in Y, X[, S]). + newOrder = [k for k in arrayOrder if k not in {'C', 'X', 'Y', 'S'}] + ( + ['C'] if 'C' in arrayOrder else []) + ['Y', 'X'] + ( + ['S'] if 'S' in arrayOrder else []) + if newOrder != arrayOrder: + self._nd2array = np.moveaxis( + self._nd2array, + list(range(len(arrayOrder))), + [newOrder.index(k) for k in arrayOrder]) + self._nd2order = newOrder + self._nd2origindex = {} + basis = 1 + for k in arrayOrder: + if k not in {'C', 'X', 'Y', 'S'}: + self._nd2origindex[k] = basis + basis *= self._nd2.sizes[k] + self.sizeX = self._nd2.sizes['X'] + self.sizeY = self._nd2.sizes['Y'] + self._nd2sizes = self._nd2.sizes + self.tileWidth = self.tileHeight = self._tileSize + if self.sizeX <= self._singleTileThreshold and self.sizeY <= self._singleTileThreshold: + self.tileWidth = self.sizeX + self.tileHeight = self.sizeY + self.levels = int(max(1, math.ceil(math.log( + float(max(self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2)) + 1)) + try: + self._frameCount = ( + self._nd2.metadata.contents.channelCount * self._nd2.metadata.contents.frameCount) + self._bandnames = { + chan.channel.name.lower(): idx + for idx, chan in enumerate(self._nd2.metadata.channels)} + self._channels = [chan.channel.name for chan in self._nd2.metadata.channels] + except Exception: + self._frameCount = basis * self._nd2.sizes.get('C', 1) + self._channels = None + if not self._validateArrayAccess(): + self._nd2.close() + del self._nd2 + msg = 'File cannot be parsed with the nd2 source. Is it a legacy nd2 file?' + raise TileSourceError(msg) + self._tileLock = threading.RLock() + + def __del__(self): + # If we have an _unstyledInstance attribute, this is not the owner of + # the _nd2 handle, so we can't close it. Otherwise, we need to close + # it or the nd2 library complains that we didn't explicitly close it. + if hasattr(self, '_nd2') and not hasattr(self, '_derivedSource'): + self._nd2.close() + del self._nd2 + + def _validateArrayAccess(self): + check = [0] * len(self._nd2order) + count = 1 + for axisidx in range(len(self._nd2order) - 1, -1, -1): + axis = self._nd2order[axisidx] + axisSize = self._nd2.sizes[axis] + check[axisidx] = axisSize - 1 + try: + self._nd2array[tuple(check)].compute(scheduler='single-threaded') + if axis not in {'X', 'Y', 'S'}: + count *= axisSize + continue + except Exception: + if axis in {'X', 'Y', 'S'}: + return False + minval = 0 + maxval = axisSize - 1 + while minval + 1 < maxval: + nextval = (minval + maxval) // 2 + check[axisidx] = nextval + try: + self._nd2array[tuple(check)].compute(scheduler='single-threaded') + minval = nextval + except Exception: + maxval = nextval + check[axisidx] = minval + self._nd2sizes = {k: check[idx] + 1 for idx, k in enumerate(self._nd2order)} + self._frameCount = (minval + 1) * count + return True + self._frameCount = count + return True + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + mm_x = mm_y = None + microns = None + try: + microns = self._nd2.voxel_size() + mm_x = microns.x * 0.001 + mm_y = microns.y * 0.001 + except Exception: + pass + # Estimate the magnification; we don't have a direct value + mag = 0.01 / mm_x if mm_x else None + return { + 'magnification': mag, + 'mm_x': mm_x, + 'mm_y': mm_y, + }
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + if not hasattr(self, '_computedMetadata'): + result = super().getMetadata() + + sizes = self._nd2.sizes + axes = self._nd2order[:self._nd2order.index('Y')][::-1] + sizes = self._nd2sizes + result['frames'] = frames = [] + for idx in range(self._frameCount): + frame = {'Frame': idx} + basis = 1 + ref = {} + for axis in axes: + ref[axis] = (idx // basis) % sizes[axis] + frame['Index' + (axis.upper() if axis.upper() != 'P' else 'XY')] = ( + idx // basis) % sizes[axis] + basis *= sizes.get(axis, 1) + frames.append(frame) + self._addMetadataFrameInformation(result, self._channels) + self._computedMetadata = result + return self._computedMetadata
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + result = {} + result['nd2'] = namedtupleToDict(self._nd2.metadata) + result['nd2_sizes'] = self._nd2.sizes + result['nd2_text'] = self._nd2.text_info + result['nd2_custom'] = self._nd2.custom_data + result['nd2_experiment'] = namedtupleToDict(self._nd2.experiment) + result['nd2_legacy'] = self._nd2.is_legacy + result['nd2_rgb'] = self._nd2.is_rgb + result['nd2_frame_metadata'] = [] + try: + for idx in range(self._nd2.metadata.contents.frameCount): + result['nd2_frame_metadata'].append(diffObj(namedtupleToDict( + self._nd2.frame_metadata(idx)), result['nd2'])) + except Exception: + pass + if (len(result['nd2_frame_metadata']) and + list(result['nd2_frame_metadata'][0].keys()) == ['channels']): + result['nd2_frame_metadata'] = [ + fm['channels'][0] for fm in result['nd2_frame_metadata']] + return result
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame, self._frameCount) + x0, y0, x1, y1, step = self._xyzToCorners(x, y, z) + tileframe = self._nd2array + fc = self._frameCount + fp = frame + for axis in self._nd2order[:self._nd2order.index('Y')]: + fc //= self._nd2sizes[axis] + tileframe = tileframe[fp // fc] + fp = fp % fc + with self._tileLock: + # Have dask use single-threaded since we are using a lock anyway. + tile = tileframe[y0:y1:step, x0:x1:step].compute(scheduler='single-threaded').copy() + return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return ND2FileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return ND2FileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_nd2/girder_source.html b/_modules/large_image_source_nd2/girder_source.html new file mode 100644 index 000000000..f6bc3ec29 --- /dev/null +++ b/_modules/large_image_source_nd2/girder_source.html @@ -0,0 +1,153 @@ + + + + + + large_image_source_nd2.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_nd2.girder_source

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import ND2FileTileSource
+
+
+
+[docs] +class ND2GirderTileSource(ND2FileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with an ND2 file or other files that + the nd2 library can read. + """ + + cacheName = 'tilesource' + name = 'nd2' + + _mayHaveAdjacentFiles = True
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_ometiff.html b/_modules/large_image_source_ometiff.html new file mode 100644 index 000000000..6b58f333b --- /dev/null +++ b/_modules/large_image_source_ometiff.html @@ -0,0 +1,549 @@ + + + + + + large_image_source_ometiff — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_ometiff

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import copy
+import math
+import os
+from collections import OrderedDict
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import numpy as np
+import PIL.Image
+from large_image_source_tiff import TiffFileTileSource
+from large_image_source_tiff.exceptions import InvalidOperationTiffError, IOTiffError, TiffError
+
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_NUMPY, TILE_FORMAT_PIL, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+_omeUnitsToMeters = {
+    'Ym': 1e24,
+    'Zm': 1e21,
+    'Em': 1e18,
+    'Pm': 1e15,
+    'Tm': 1e12,
+    'Gm': 1e9,
+    'Mm': 1e6,
+    'km': 1e3,
+    'hm': 1e2,
+    'dam': 1e1,
+    'm': 1,
+    'dm': 1e-1,
+    'cm': 1e-2,
+    'mm': 1e-3,
+    '\u00b5m': 1e-6,
+    'nm': 1e-9,
+    'pm': 1e-12,
+    'fm': 1e-15,
+    'am': 1e-18,
+    'zm': 1e-21,
+    'ym': 1e-24,
+    '\u00c5': 1e-10,
+}
+
+
+
+[docs] +class OMETiffFileTileSource(TiffFileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to TIFF files. + """ + + cacheName = 'tilesource' + name = 'ometiff' + extensions = { + None: SourcePriority.LOW, + 'tif': SourcePriority.MEDIUM, + 'tiff': SourcePriority.MEDIUM, + 'ome': SourcePriority.PREFERRED, + } + mimeTypes = { + 'image/tiff': SourcePriority.MEDIUM, + 'image/x-tiff': SourcePriority.MEDIUM, + } + + # The expect number of pixels that would need to be read to read the worst- + # case tile. + _maxUntiledChunk = 512 * 1024 * 1024 + + def __init__(self, path, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + # Note this is the super of the parent class, not of this class. + super(TiffFileTileSource, self).__init__(path, **kwargs) + + self._largeImagePath = str(self._getLargeImagePath()) + + try: + base = self.getTiffDir(0, mustBeTiled=None) + except TiffError: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + msg = 'Not a recognized OME Tiff' + raise TileSourceError(msg) + info = getattr(base, '_description_record', None) + if not info or not info.get('OME'): + msg = 'Not an OME Tiff' + raise TileSourceError(msg) + self._omeinfo = info['OME'] + self._checkForOMEZLoop() + try: + self._parseOMEInfo() + except KeyError: + msg = 'Not a recognized OME Tiff' + raise TileSourceError(msg) + omeimages = [ + entry['Pixels'] for entry in self._omeinfo['Image'] if + len(entry['Pixels']['TiffData']) == len(self._omebase['TiffData'])] + levels = [max(0, int(math.ceil(math.log(max( + float(entry['SizeX']) / base.tileWidth, + float(entry['SizeY']) / base.tileHeight)) / math.log(2)))) + for entry in omeimages] + omebylevel = dict(zip(levels, omeimages)) + self._omeLevels = [omebylevel.get(key) for key in range(max(omebylevel.keys()) + 1)] + if base._tiffInfo.get('istiled'): + self._tiffDirectories = [ + self.getTiffDir(int(entry['TiffData'][0].get('IFD', 0))) + if entry else None + for entry in self._omeLevels] + else: + self._tiffDirectories = [ + self.getTiffDir(0, mustBeTiled=None) + if entry else None + for entry in self._omeLevels] + self._checkForInefficientDirectories(warn=False) + _maxChunk = min(base.imageWidth, base.tileWidth * self._skippedLevels ** 2) * \ + min(base.imageHeight, base.tileHeight * self._skippedLevels ** 2) + if _maxChunk > self._maxUntiledChunk: + msg = 'Untiled image is too large to access with the OME Tiff source' + raise TileSourceError(msg) + self.tileWidth = base.tileWidth + self.tileHeight = base.tileHeight + self.levels = len(self._tiffDirectories) + self.sizeX = base.imageWidth + self.sizeY = base.imageHeight + + # We can get the embedded images, but we don't currently use non-tiled + # images as associated images. This would require enumerating tiff + # directories not mentioned by the ome list. + self._associatedImages = {} + self._checkForInefficientDirectories() + + def _checkForOMEZLoop(self): + """ + Check if the OME description lists a Z-loop that isn't referenced by + the frames or TiffData list and is present based on the number of tiff + directories. This can modify self._omeinfo. + """ + info = self._omeinfo + try: + zloopinfo = info['Image']['Description'].split('Z Stack Loop: ')[1] + zloop = int(zloopinfo.split()[0]) + stepinfo = zloopinfo.split('Step: ')[1].split() + stepmm = float(stepinfo[0]) + stepmm *= {'mm': 1, '\xb5m': 0.001}[stepinfo[1]] + planes = len(info['Image']['Pixels']['Plane']) + for plane in info['Image']['Pixels']['Plane']: + if int(plane.get('TheZ', 0)) != 0: + return + if int(info['Image']['Pixels']['SizeZ']) != 1: + return + except Exception: + return + if zloop <= 1 or not stepmm or not planes: + return + if len(info['Image']['Pixels'].get('TiffData', {})): + return + expecteddir = planes * zloop + try: + lastdir = self.getTiffDir(expecteddir - 1, mustBeTiled=None) + if not lastdir._tiffInfo.get('lastdirectory'): + return + except Exception: + return + tiffdata = [] + for z in range(zloop): + for plane in info['Image']['Pixels']['Plane']: + td = plane.copy() + td['TheZ'] = str(z) + # This position is probably wrong -- it seems like the + # listed position is likely to be the center of the stack, not + # the bottom, but we'd have to confirm it. + td['PositionZ'] = str(float(td.get('PositionZ', 0)) + z * stepmm * 1000) + tiffdata.append(td) + info['Image']['Pixels']['TiffData'] = tiffdata + info['Image']['Pixels']['Plane'] = tiffdata + info['Image']['Pixels']['PlanesFromZloop'] = 'true' + info['Image']['Pixels']['SizeZ'] = str(zloop) + + def _parseOMEInfo(self): # noqa + if isinstance(self._omeinfo['Image'], dict): + self._omeinfo['Image'] = [self._omeinfo['Image']] + for img in self._omeinfo['Image']: + if isinstance(img['Pixels'].get('TiffData'), dict): + img['Pixels']['TiffData'] = [img['Pixels']['TiffData']] + if isinstance(img['Pixels'].get('Plane'), dict): + img['Pixels']['Plane'] = [img['Pixels']['Plane']] + if isinstance(img['Pixels'].get('Channels'), dict): + img['Pixels']['Channels'] = [img['Pixels']['Channels']] + try: + self._omebase = self._omeinfo['Image'][0]['Pixels'] + if isinstance(self._omebase.get('Plane'), dict): + self._omebase['Plane'] = [self._omebase['Plane']] + if ((not len(self._omebase['TiffData']) or + len(self._omebase['TiffData']) == 1) and + (len(self._omebase.get('Plane', [])) or + len(self._omebase.get('Channel', [])))): + if (not len(self._omebase['TiffData']) or + self._omebase['TiffData'][0] == {} or + int(self._omebase['TiffData'][0].get('PlaneCount', 0)) == 1): + planes = copy.deepcopy(self._omebase.get( + 'Plane', self._omebase.get('Channel'))) + if isinstance(planes, dict): + planes = [planes] + self._omebase['SizeC'] = 1 + for idx, plane in enumerate(planes): + plane['IndexC'] = idx + self._omebase['TiffData'] = planes + elif (int(self._omebase['TiffData'][0].get('PlaneCount', 0)) == + len(self._omebase.get('Plane', self._omebase.get('Channel', [])))): + planes = copy.deepcopy(self._omebase.get('Plane', self._omebase.get('Channel'))) + for idx, plane in enumerate(planes): + plane['IFD'] = plane.get( + 'IFD', int(self._omebase['TiffData'][0].get('IFD', 0)) + idx) + self._omebase['TiffData'] = planes + if isinstance(self._omebase['TiffData'], dict): + self._omebase['TiffData'] = [self._omebase['TiffData']] + if len({entry.get('UUID', {}).get('FileName', '') + for entry in self._omebase['TiffData']}) > 1: + msg = 'OME Tiff references multiple files' + raise TileSourceError(msg) + if (len(self._omebase['TiffData']) != int(self._omebase['SizeC']) * + int(self._omebase['SizeT']) * int(self._omebase['SizeZ']) or + len(self._omebase['TiffData']) != len( + self._omebase.get('Plane', self._omebase['TiffData']))): + msg = 'OME Tiff contains frames that contain multiple planes' + raise TileSourceError(msg) + except (KeyError, ValueError, IndexError, TypeError): + msg = 'OME Tiff does not contain an expected record' + raise TileSourceError(msg) + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + result = super().getMetadata() + result['frames'] = copy.deepcopy(self._omebase.get('Plane', self._omebase['TiffData'])) + channels = [] + for img in self._omeinfo['Image']: + try: + channels = [channel['Name'] for channel in img['Pixels']['Channel']] + if len(channels) > 1: + break + except Exception: + pass + if len(set(channels)) != len(channels) and ( + len(channels) <= 1 or len(channels) > len(result['frames'])): + channels = [] + for k in {'C', 'Z', 'T'}: + if (str(len(result['frames'])) == str(self._omebase.get('Size%s' % k)) and + len(result['frames']) > 1 and + result['frames'][0].get('Index%s' % k) is None): + for idx in range(len(result['frames'])): + result['frames'][idx]['Index%s' % k] = idx + # Standardize "TheX" to "IndexX" values + reftbl = OrderedDict([ + ('TheC', 'IndexC'), ('TheZ', 'IndexZ'), ('TheT', 'IndexT'), + ('FirstC', 'IndexC'), ('FirstZ', 'IndexZ'), ('FirstT', 'IndexT'), + ]) + for frame in result['frames']: + for key in reftbl: + if key in frame and reftbl[key] not in frame: + frame[reftbl[key]] = int(frame[key]) + frame.pop(key, None) + self._addMetadataFrameInformation(result, channels) + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + return {'omeinfo': self._omeinfo}
+ + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification for the highest-resolution level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + result = super().getNativeMagnification() + if result['mm_x'] is None and 'PhysicalSizeX' in self._omebase: + result['mm_x'] = ( + float(self._omebase['PhysicalSizeX']) * 1e3 * + _omeUnitsToMeters[self._omebase.get('PhysicalSizeXUnit', '\u00b5m')]) + if result['mm_y'] is None and 'PhysicalSizeY' in self._omebase: + result['mm_y'] = ( + float(self._omebase['PhysicalSizeY']) * 1e3 * + _omeUnitsToMeters[self._omebase.get('PhysicalSizeYUnit', '\u00b5m')]) + if not result.get('magnification') and result.get('mm_x'): + result['magnification'] = 0.01 / result['mm_x'] + return result
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, + sparseFallback=False, **kwargs): + if ((z < 0 or z >= len(self._omeLevels) or ( + self._omeLevels[z] is not None and kwargs.get('frame') in (None, 0, '0', ''))) and + not getattr(self, '_style', None)): + return super().getTile( + x, y, z, pilImageAllowed=pilImageAllowed, + numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, + **kwargs) + frame = self._getFrame(**kwargs) + if frame < 0 or frame >= len(self._omebase['TiffData']): + msg = 'Frame does not exist' + raise TileSourceError(msg) + subdir = None + if self._omeLevels[z] is not None: + dirnum = int(self._omeLevels[z]['TiffData'][frame].get('IFD', frame)) + else: + dirnum = int(self._omeLevels[-1]['TiffData'][frame].get('IFD', frame)) + subdir = self.levels - 1 - z + dir = self._getDirFromCache(dirnum, subdir) + if subdir: + scale = int(2 ** subdir) + if (dir is None or + (dir.tileWidth != self.tileWidth and dir.tileWidth != dir.imageWidth) or + (dir.tileHeight != self.tileHeight and dir.tileHeight != dir.imageHeight) or + abs(dir.imageWidth * scale - self.sizeX) > scale or + abs(dir.imageHeight * scale - self.sizeY) > scale): + return super().getTile( + x, y, z, pilImageAllowed=pilImageAllowed, + numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, + **kwargs) + try: + tile = dir.getTile(x, y, asarray=numpyAllowed == 'always') + format = 'JPEG' + if isinstance(tile, PIL.Image.Image): + format = TILE_FORMAT_PIL + if isinstance(tile, np.ndarray): + format = TILE_FORMAT_NUMPY + return self._outputTile(tile, format, x, y, z, pilImageAllowed, + numpyAllowed, **kwargs) + except InvalidOperationTiffError as e: + raise TileSourceError(e.args[0]) + except IOTiffError as e: + return self.getTileIOTiffError( + x, y, z, pilImageAllowed=pilImageAllowed, + numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, + exception=e, **kwargs)
+ + +
+[docs] + def getPreferredLevel(self, level): + """ + Given a desired level (0 is minimum resolution, self.levels - 1 is max + resolution), return the level that contains actual data that is no + lower resolution. + + :param level: desired level + :returns level: a level with actual data that is no lower resolution. + """ + level = max(0, min(level, self.levels - 1)) + baselevel = level + while self._tiffDirectories[level] is None and level < self.levels - 1: + try: + dirnum = int(self._omeLevels[-1]['TiffData'][0].get('IFD', 0)) + subdir = self.levels - 1 - level + if self._getDirFromCache(dirnum, subdir): + break + except Exception: + pass + level += 1 + while level - baselevel > self._maxSkippedLevels: + level -= self._maxSkippedLevels + return level
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return OMETiffFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return OMETiffFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_ometiff/girder_source.html b/_modules/large_image_source_ometiff/girder_source.html new file mode 100644 index 000000000..66496bdd1 --- /dev/null +++ b/_modules/large_image_source_ometiff/girder_source.html @@ -0,0 +1,150 @@ + + + + + + large_image_source_ometiff.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_ometiff.girder_source

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import OMETiffFileTileSource
+
+
+
+[docs] +class OMETiffGirderTileSource(OMETiffFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with an OMETiff file. + """ + + cacheName = 'tilesource' + name = 'ometiff'
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_openjpeg.html b/_modules/large_image_source_openjpeg.html new file mode 100644 index 000000000..12a4841d1 --- /dev/null +++ b/_modules/large_image_source_openjpeg.html @@ -0,0 +1,453 @@ + + + + + + large_image_source_openjpeg — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_openjpeg

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import builtins
+import io
+import math
+import os
+import queue
+import struct
+import warnings
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+from xml.etree import ElementTree
+
+import PIL.Image
+
+import large_image
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource, etreeToDict
+from large_image.tilesource.utilities import _imageToNumpy
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+glymur = None
+
+
+def _lazyImport():
+    """
+    Import the glymur module.  This is done when needed rather than in the module
+    initialization because it is slow.
+    """
+    global glymur
+
+    if glymur is None:
+        try:
+            import glymur
+        except ImportError:
+            msg = 'glymur module not found.'
+            raise TileSourceError(msg)
+
+
+warnings.filterwarnings('ignore', category=UserWarning, module='glymur')
+
+
+
+[docs] +class OpenjpegFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to jp2 files and other files the openjpeg library can + read. + """ + + cacheName = 'tilesource' + name = 'openjpeg' + extensions = { + None: SourcePriority.MEDIUM, + 'jp2': SourcePriority.PREFERRED, + 'jpf': SourcePriority.PREFERRED, + 'j2k': SourcePriority.PREFERRED, + 'jpx': SourcePriority.PREFERRED, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'image/jp2': SourcePriority.PREFERRED, + 'image/jpx': SourcePriority.PREFERRED, + } + + _boxToTag = { + # In the few samples I've seen, both of these appear to be macro images + b'mig ': 'macro', + b'mag ': 'label', + # This contains a largish image + # b'psi ': 'other', + } + _xmlTag = b'mxl ' + + _minTileSize = 256 + _maxTileSize = 512 + _maxOpenHandles = 6 + + def __init__(self, path, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + _lazyImport() + self._largeImagePath = str(self._getLargeImagePath()) + self._pixelInfo = {} + try: + self._openjpeg = glymur.Jp2k(self._largeImagePath) + if not self._openjpeg.shape: + if not os.path.isfile(self._largeImagePath): + raise FileNotFoundError + msg = 'File cannot be opened via Glymur and OpenJPEG (no shape).' + raise TileSourceError(msg) + except (glymur.jp2box.InvalidJp2kError, struct.error): + msg = 'File cannot be opened via Glymur and OpenJPEG.' + raise TileSourceError(msg) + except FileNotFoundError: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + raise + glymur.set_option('lib.num_threads', large_image.config.cpu_count()) + self._openjpegHandles = queue.LifoQueue() + for _ in range(self._maxOpenHandles - 1): + self._openjpegHandles.put(None) + self._openjpegHandles.put(self._openjpeg) + try: + self.sizeY, self.sizeX = self._openjpeg.shape[:2] + except IndexError as exc: + raise TileSourceError('File cannot be opened via Glymur and OpenJPEG: %r' % exc) + self.levels = int(self._openjpeg.codestream.segment[2].num_res) + 1 + self._minlevel = 0 + self.tileWidth = self.tileHeight = 2 ** int(math.ceil(max( + math.log(float(self.sizeX)) / math.log(2) - self.levels + 1, + math.log(float(self.sizeY)) / math.log(2) - self.levels + 1))) + # Small and large tiles are both inefficient. Large tiles don't work + # with some viewers (leaflet and Slide Atlas, for instance) + if self.tileWidth < self._minTileSize or self.tileWidth > self._maxTileSize: + self.tileWidth = self.tileHeight = min( + self._maxTileSize, max(self._minTileSize, self.tileWidth)) + self.levels = int(math.ceil(math.log(float(max( + self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2))) + 1 + self._minlevel = self.levels - self._openjpeg.codestream.segment[2].num_res - 1 + self._getAssociatedImages() + self._populatedLevels = self.levels - self._minlevel + + def _getAssociatedImages(self): + """ + Read associated images and metadata from boxes. + """ + self._associatedImages = {} + for box in self._openjpeg.box: + box_id = box.box_id + if box_id == 'xxxx': + box_id = getattr(box, 'claimed_box_id', box.box_id) + if box_id == self._xmlTag or box_id in self._boxToTag: + data = self._readbox(box) + if data is None: + continue + if box_id == self._xmlTag: + self._parseMetadataXml(data) + continue + try: + self._associatedImages[self._boxToTag[box_id]] = PIL.Image.open( + io.BytesIO(data)) + except Exception: + pass + if box_id == 'jp2c': + for segment in box.codestream.segment: + if segment.marker_id == 'CME' and hasattr(segment, 'ccme'): + self._parseMetadataXml(segment.ccme) + if hasattr(box, 'box'): + for subbox in box.box: + if getattr(subbox, 'icc_profile', None): + self._iccprofiles = [subbox.icc_profile] + + def _nonemptyLevelsList(self, frame=0): + """ + Return a list of one value per level where the value is None if the + level does not exist in the file and any other value if it does. + + :param frame: the frame number. + :returns: a list of levels length. + """ + return [True if self.levels - 1 - idx < self._populatedLevels else None + for idx in range(self.levels)] + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + mm_x = self._pixelInfo.get('mm_x') + mm_y = self._pixelInfo.get('mm_y') + # Estimate the magnification if we don't have a direct value + mag = self._pixelInfo.get('magnification') or 0.01 / mm_x if mm_x else None + return { + 'magnification': mag, + 'mm_x': mm_x, + 'mm_y': mm_y, + }
+ + + def _parseMetadataXml(self, meta): + if not isinstance(meta, str): + meta = meta.decode('utf8', 'ignore') + try: + xml = ElementTree.fromstring(meta) + except Exception: + return + self._description_record = etreeToDict(xml) + xml = self._description_record + try: + # Optrascan metadata + scanDetails = xml.get('ScanInfo', xml.get('EncodeInfo'))['ScanDetails'] + mag = float(scanDetails['Magnification']) + # In microns; convert to mm + scale = float(scanDetails['PixelResolution']) * 1e-3 + self._pixelInfo = { + 'magnification': mag, + 'mm_x': scale, + 'mm_y': scale, + } + except Exception: + pass + + def _getAssociatedImage(self, imageKey): + """ + Get an associated image in PIL format. + + :param imageKey: the key of the associated image. + :return: the image in PIL format or None. + """ + return self._associatedImages.get(imageKey) + +
+[docs] + def getAssociatedImagesList(self): + """ + Return a list of associated images. + + :return: the list of image keys. + """ + return sorted(self._associatedImages.keys())
+ + + def _readbox(self, box): + if box.length > 16 * 1024 * 1024: + return + try: + fp = builtins.open(self._largeImagePath, 'rb') + headerLength = 16 + fp.seek(box.offset + headerLength) + return fp.read(box.length - headerLength) + except Exception: + pass + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + results = {} + if hasattr(self, '_description_record'): + results['xml'] = self._description_record + return results
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + self._xyzInRange(x, y, z) + x0, y0, x1, y1, step = self._xyzToCorners(x, y, z) + scale = None + if self._minlevel - z > self._maxSkippedLevels: + tile = self._getTileFromEmptyLevel(x, y, z, **kwargs) + tile = _imageToNumpy(tile)[0] + else: + if z < self._minlevel: + scale = int(2 ** (self._minlevel - z)) + step = int(2 ** (self.levels - 1 - self._minlevel)) + # possibly open the file multiple times so multiple threads can access + # it concurrently. + while True: + try: + # A timeout prevents uninterupptable waits on some platforms + openjpegHandle = self._openjpegHandles.get(timeout=1.0) + break + except queue.Empty: + continue + if openjpegHandle is None: + openjpegHandle = glymur.Jp2k(self._largeImagePath) + try: + tile = openjpegHandle[y0:y1:step, x0:x1:step] + finally: + self._openjpegHandles.put(openjpegHandle) + if scale: + tile = tile[::scale, ::scale] + return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return OpenjpegFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return OpenjpegFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_openjpeg/girder_source.html b/_modules/large_image_source_openjpeg/girder_source.html new file mode 100644 index 000000000..23bd586b9 --- /dev/null +++ b/_modules/large_image_source_openjpeg/girder_source.html @@ -0,0 +1,158 @@ + + + + + + large_image_source_openjpeg.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_openjpeg.girder_source

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import OpenjpegFileTileSource
+
+
+
+[docs] +class OpenjpegGirderTileSource(OpenjpegFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with a jp2 file or other files that + the openjpeg library can read. + """ + + cacheName = 'tilesource' + name = 'openjpeg' + +
+[docs] + def mayHaveAdjacentFiles(self, largeImageFile): + # Glymur now uses extensions to determine if it can read a file. + return True
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_openslide.html b/_modules/large_image_source_openslide.html new file mode 100644 index 000000000..f11e531dc --- /dev/null +++ b/_modules/large_image_source_openslide.html @@ -0,0 +1,592 @@ + + + + + + large_image_source_openslide — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_openslide

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import io
+import math
+import os
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import openslide
+import PIL
+import tifftools
+
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_PIL, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource, nearPowerOfTwo
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+
+[docs] +class OpenslideFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to SVS files and other files the openslide library can + read. + """ + + cacheName = 'tilesource' + name = 'openslide' + extensions = { + None: SourcePriority.MEDIUM, + 'bif': SourcePriority.LOW, # Ventana + 'dcm': SourcePriority.LOW, # DICOM + 'ini': SourcePriority.LOW, # Part of mrxs + 'mrxs': SourcePriority.PREFERRED, # MIRAX + 'ndpi': SourcePriority.PREFERRED, # Hamamatsu + 'scn': SourcePriority.LOW, # Leica + 'svs': SourcePriority.PREFERRED, + 'svslide': SourcePriority.PREFERRED, + 'tif': SourcePriority.MEDIUM, + 'tiff': SourcePriority.MEDIUM, + 'vms': SourcePriority.HIGH, # Hamamatsu + 'vmu': SourcePriority.HIGH, # Hamamatsu + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'image/mirax': SourcePriority.PREFERRED, # MIRAX + 'image/tiff': SourcePriority.MEDIUM, + 'image/x-tiff': SourcePriority.MEDIUM, + } + + def __init__(self, path, **kwargs): # noqa + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + self._largeImagePath = str(self._getLargeImagePath()) + + try: + self._openslide = openslide.OpenSlide(self._largeImagePath) + except openslide.lowlevel.OpenSlideUnsupportedFormatError: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + msg = 'File cannot be opened via OpenSlide.' + raise TileSourceError(msg) + except openslide.lowlevel.OpenSlideError: + msg = 'File will not be opened via OpenSlide.' + raise TileSourceError(msg) + try: + self._tiffinfo = tifftools.read_tiff(self._largeImagePath) + if tifftools.Tag.ICCProfile.value in self._tiffinfo['ifds'][0]['tags']: + self._iccprofiles = [self._tiffinfo['ifds'][0]['tags'][ + tifftools.Tag.ICCProfile.value]['data']] + except Exception: + pass + + svsAvailableLevels = self._getAvailableLevels(self._largeImagePath) + if not len(svsAvailableLevels): + msg = 'OpenSlide image size is invalid.' + raise TileSourceError(msg) + self.sizeX = svsAvailableLevels[0]['width'] + self.sizeY = svsAvailableLevels[0]['height'] + if (self.sizeX != self._openslide.dimensions[0] or + self.sizeY != self._openslide.dimensions[1]): + msg = ('OpenSlide reports a dimension of %d x %d, but base layer ' + 'has a dimension of %d x %d -- using base layer ' + 'dimensions.' % ( + self._openslide.dimensions[0], + self._openslide.dimensions[1], self.sizeX, self.sizeY)) + self.logger.info(msg) + + self._getTileSize() + + self.levels = int(math.ceil(max( + math.log(float(self.sizeX) / self.tileWidth), + math.log(float(self.sizeY) / self.tileHeight)) / math.log(2))) + 1 + if self.levels < 1: + msg = 'OpenSlide image must have at least one level.' + raise TileSourceError(msg) + self._svslevels = [] + # Precompute which SVS level should be used for our tile levels. SVS + # level 0 is the maximum resolution. The SVS levels are in descending + # resolution and, we assume, are powers of two in scale. For each of + # our levels (where 0 is the minimum resolution), find the lowest + # resolution SVS level that contains at least as many pixels. If this + # is not the same scale as we expect, note the scale factor so we can + # load an appropriate area and scale it to the tile size later. + maxSize = 16384 # This should probably be based on available memory + for level in range(self.levels): + levelW = max(1, self.sizeX / 2 ** (self.levels - 1 - level)) + levelH = max(1, self.sizeY / 2 ** (self.levels - 1 - level)) + # bestlevel and scale will be the picked svs level and the scale + # between that level and what we really wanted. We expect scale to + # always be a positive integer power of two. + bestlevel = svsAvailableLevels[0]['level'] + scale = 1 + for svslevel in range(len(svsAvailableLevels)): + if (svsAvailableLevels[svslevel]['width'] < levelW - 1 or + svsAvailableLevels[svslevel]['height'] < levelH - 1): + break + bestlevel = svsAvailableLevels[svslevel]['level'] + scale = int(round(svsAvailableLevels[svslevel]['width'] / levelW)) + # If there are no tiles at a particular level, we have to read a + # larger area of a higher resolution level. If such an area would + # be excessively large, we could have memory issues, so raise an + # error. + if (self.tileWidth * scale > maxSize or + self.tileHeight * scale > maxSize): + msg = ('OpenSlide has no small-scale tiles (level %d is at %d ' + 'scale)' % (level, scale)) + self.logger.info(msg) + raise TileSourceError(msg) + self._svslevels.append({ + 'svslevel': bestlevel, + 'scale': scale, + }) + self._bounds = None + try: + bounds = { + 'x': int(self._openslide.properties[openslide.PROPERTY_NAME_BOUNDS_X]), + 'y': int(self._openslide.properties[openslide.PROPERTY_NAME_BOUNDS_Y]), + 'width': int(self._openslide.properties[openslide.PROPERTY_NAME_BOUNDS_WIDTH]), + 'height': int(self._openslide.properties[openslide.PROPERTY_NAME_BOUNDS_HEIGHT]), + } + if ( + bounds['x'] >= 0 and bounds['width'] > 0 and + bounds['x'] + bounds['width'] <= self.sizeX and + bounds['y'] >= 0 and bounds['height'] > 0 and + bounds['y'] + bounds['height'] <= self.sizeY and + (bounds['width'] < self.sizeX or bounds['height'] < self.sizeY) + ): + self._bounds = bounds + self.sizeX, self.sizeY = bounds['width'], bounds['height'] + prevlevels = self.levels + self.levels = int(math.ceil(max( + math.log(float(self.sizeX) / self.tileWidth), + math.log(float(self.sizeY) / self.tileHeight)) / math.log(2))) + 1 + self._svslevels = self._svslevels[prevlevels - self.levels:] + except Exception: + pass + self._populatedLevels = len({l['svslevel'] for l in self._svslevels}) + + def _getTileSize(self): + """ + Get the tile size. The tile size isn't in the official openslide + interface documentation, but every example has the tile size in the + properties. If the tile size has an excessive aspect ratio or isn't + set, fall back to a default of 256 x 256. The read_region function + abstracts reading the tiles, so this may be less efficient, but will + still work. + """ + # Try to read it, but fall back to 256 if it isn't set. + width = height = 256 + try: + width = int(self._openslide.properties[ + 'openslide.level[0].tile-width']) + except (ValueError, KeyError): + pass + try: + height = int(self._openslide.properties[ + 'openslide.level[0].tile-height']) + except (ValueError, KeyError): + pass + # If the tile size is too small (<4) or wrong (<=0), use a default value + if width < 4: + width = 256 + if height < 4: + height = 256 + # If the tile has an excessive aspect ratio, use default values + if max(width, height) / min(width, height) >= 4: + width = height = 256 + # Don't let tiles be bigger than the whole image. + self.tileWidth = min(width, self.sizeX) + self.tileHeight = min(height, self.sizeY) + + def _getAvailableLevels(self, path): + """ + Some SVS files (notably some NDPI variants) have levels that cannot be + read. Get a list of levels, check that each is at least potentially + readable, and return a list of these sorted highest-resolution first. + + :param path: the path of the SVS file. After a failure, the file is + reopened to reset the error state. + :returns: levels. A list of valid levels, each of which is a + dictionary of level (the internal 0-based level number), width, and + height. + """ + levels = [] + svsLevelDimensions = self._openslide.level_dimensions + for svslevel in range(len(svsLevelDimensions)): + try: + self._openslide.read_region((0, 0), svslevel, (1, 1)) + level = { + 'level': svslevel, + 'width': svsLevelDimensions[svslevel][0], + 'height': svsLevelDimensions[svslevel][1], + } + if level['width'] > 0 and level['height'] > 0: + # add to the list so that we can sort by resolution and + # then by earlier entries + levels.append((level['width'] * level['height'], -len(levels), level)) + except openslide.lowlevel.OpenSlideError: + self._openslide = openslide.OpenSlide(path) + # sort highest resolution first. + levels = [entry[-1] for entry in sorted(levels, reverse=True, key=lambda x: x[:-1])] + # Discard levels that are not a power-of-two compared to the highest + # resolution level. + levels = [entry for entry in levels if + nearPowerOfTwo(levels[0]['width'], entry['width']) and + nearPowerOfTwo(levels[0]['height'], entry['height'])] + return levels + + def _nonemptyLevelsList(self, frame=0): + """ + Return a list of one value per level where the value is None if the + level does not exist in the file and any other value if it does. + + :param frame: the frame number. + :returns: a list of levels length. + """ + return [True if l['scale'] == 1 else None for l in self._svslevels] + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + try: + mag = self._openslide.properties[ + openslide.PROPERTY_NAME_OBJECTIVE_POWER] + mag = float(mag) if mag else None + except (KeyError, ValueError, openslide.lowlevel.OpenSlideError): + mag = None + try: + mm_x = float(self._openslide.properties[ + openslide.PROPERTY_NAME_MPP_X]) * 0.001 + mm_y = float(self._openslide.properties[ + openslide.PROPERTY_NAME_MPP_Y]) * 0.001 + except Exception: + mm_x = mm_y = None + # Estimate the magnification if we don't have a direct value + if mag is None and mm_x is not None: + mag = 0.01 / mm_x + return { + 'magnification': mag, + 'mm_x': mm_x, + 'mm_y': mm_y, + }
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + results = {'openslide': {}} + for key in self._openslide.properties: + results['openslide'][key] = self._openslide.properties[key] + if key == 'openslide.comment': + leader = self._openslide.properties[key].split('\n', 1)[0].strip() + if 'aperio' in leader.lower(): + results['aperio_version'] = leader + return results
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + self._xyzInRange(x, y, z) + svslevel = self._svslevels[z] + # When we read a region from the SVS, we have to ask for it in the + # SVS level 0 coordinate system. Our x and y is in tile space at the + # specified z level, so the offset in SVS level 0 coordinates has to be + # scaled by the tile size and by the z level. + scale = 2 ** (self.levels - 1 - z) + offsetx = x * self.tileWidth * scale + offsety = y * self.tileHeight * scale + if self._bounds is not None: + offsetx += self._bounds['x'] // svslevel['scale'] + offsety += self._bounds['y'] // svslevel['scale'] + # We ask to read an area that will cover the tile at the z level. The + # scale we computed in the __init__ process for this svs level tells + # how much larger a region we need to read. + if svslevel['scale'] > 2 ** self._maxSkippedLevels: + tile = self._getTileFromEmptyLevel(x, y, z, **kwargs) + else: + retries = 3 + while retries > 0: + try: + tile = self._openslide.read_region( + (offsetx, offsety), svslevel['svslevel'], + (self.tileWidth * svslevel['scale'], + self.tileHeight * svslevel['scale'])) + break + except openslide.lowlevel.OpenSlideError as exc: + self._largeImagePath = str(self._getLargeImagePath()) + msg = ( + 'Failed to get OpenSlide region ' + f'({exc} on {self._largeImagePath}: {self}).') + self.logger.info(msg) + # Reopen handle after a lowlevel error + try: + self._openslide = openslide.OpenSlide(self._largeImagePath) + except Exception: + raise TileSourceError(msg) + retries -= 1 + if retries <= 0: + raise TileSourceError(msg) + # Always scale to the svs level 0 tile size. + if svslevel['scale'] != 1: + tile = tile.resize((self.tileWidth, self.tileHeight), + getattr(PIL.Image, 'Resampling', PIL.Image).LANCZOS) + return self._outputTile(tile, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, + numpyAllowed, **kwargs)
+ + +
+[docs] + def getPreferredLevel(self, level): + """ + Given a desired level (0 is minimum resolution, self.levels - 1 is max + resolution), return the level that contains actual data that is no + lower resolution. + + :param level: desired level + :returns level: a level with actual data that is no lower resolution. + """ + level = max(0, min(level, self.levels - 1)) + scale = self._svslevels[level]['scale'] + while scale > 1: + level += 1 + scale /= 2 + return level
+ + + def _getAssociatedImagesDict(self): + images = {} + try: + for key in self._openslide.associated_images: + images[key] = 'openslide' + except openslide.lowlevel.OpenSlideError: + pass + if hasattr(self, '_tiffinfo'): + vendor = self._openslide.properties['openslide.vendor'] + for ifdidx, ifd in enumerate(self._tiffinfo['ifds']): + key = None + if vendor == 'hamamatsu': + if tifftools.Tag.NDPI_SOURCELENS.value in ifd['tags']: + lens = ifd['tags'][tifftools.Tag.NDPI_SOURCELENS.value]['data'][0] + key = {-1: 'macro', -2: 'nonempty'}.get(lens) + elif vendor == 'aperio': + if (ifd['tags'].get(tifftools.Tag.NewSubfileType.value) and + ifd['tags'][tifftools.Tag.NewSubfileType.value]['data'][0] & + tifftools.Tag.NewSubfileType.bitfield.ReducedImage.value): + key = ('label' if ifd['tags'][ + tifftools.Tag.NewSubfileType.value]['data'][0] == + tifftools.Tag.NewSubfileType.bitfield.ReducedImage.value + else 'macro') + if key and key not in images: + images[key] = ifdidx + return images + +
+[docs] + def getAssociatedImagesList(self): + """ + Get a list of all associated images. + + :return: the list of image keys. + """ + return sorted(self._getAssociatedImagesDict().keys())
+ + + def _getAssociatedImage(self, imageKey): + """ + Get an associated image in PIL format. + + :param imageKey: the key of the associated image. + :return: the image in PIL format or None. + """ + images = self._getAssociatedImagesDict() + if imageKey not in images: + return None + if images[imageKey] == 'openslide': + try: + return self._openslide.associated_images[imageKey] + except openslide.lowlevel.OpenSlideError: + # Reopen handle after a lowlevel error + self._openslide = openslide.OpenSlide(self._largeImagePath) + return None + tiff_buffer = io.BytesIO() + ifd = self._tiffinfo['ifds'][images[imageKey]] + if (tifftools.Tag.Photometric.value in ifd['tags'] and + ifd['tags'][tifftools.Tag.Photometric.value]['data'][0] == + tifftools.constants.Photometric.RGB and + tifftools.Tag.SamplesPerPixel.value in ifd['tags'] and + ifd['tags'][tifftools.Tag.SamplesPerPixel.value]['data'][0] == 1): + ifd['tags'][tifftools.Tag.Photometric.value]['data'][ + 0] = tifftools.constants.Photometric.MinIsBlack + tifftools.write_tiff(ifd, tiff_buffer) + return PIL.Image.open(tiff_buffer)
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return OpenslideFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return OpenslideFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_openslide/girder_source.html b/_modules/large_image_source_openslide/girder_source.html new file mode 100644 index 000000000..2d4a386e8 --- /dev/null +++ b/_modules/large_image_source_openslide/girder_source.html @@ -0,0 +1,154 @@ + + + + + + large_image_source_openslide.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_openslide.girder_source

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import OpenslideFileTileSource
+
+
+
+[docs] +class OpenslideGirderTileSource(OpenslideFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with an SVS file or other files that + the openslide library can read. + """ + + cacheName = 'tilesource' + name = 'openslide' + + extensionsWithAdjacentFiles = {'mrxs'} + mimeTypesWithAdjacentFiles = {'image/mirax'}
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_pil.html b/_modules/large_image_source_pil.html new file mode 100644 index 000000000..7e2cdae0b --- /dev/null +++ b/_modules/large_image_source_pil.html @@ -0,0 +1,468 @@ + + + + + + large_image_source_pil — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_pil

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import contextlib
+import json
+import math
+import os
+import threading
+
+import numpy as np
+import PIL.Image
+
+import large_image
+from large_image import config
+from large_image.cache_util import LruCacheMetaclass, methodcache, strhash
+from large_image.constants import TILE_FORMAT_PIL, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource
+
+# Optionally extend PIL with some additional formats
+try:
+    from pillow_heif import register_heif_opener
+    register_heif_opener()
+    from pillow_heif import register_avif_opener
+    register_avif_opener()
+except Exception:
+    pass
+try:
+    import pillow_jxl  # noqa
+except Exception:
+    pass
+try:
+    import pillow_jpls  # noqa
+except Exception:
+    pass
+
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+# Default to ignoring files with some specific extensions.
+config.ConfigValues['source_pil_ignored_names'] = \
+    r'(\.mrxs|\.vsi)$'
+
+
+
+[docs] +def getMaxSize(size=None, maxDefault=4096): + """ + Get the maximum width and height that we allow for an image. + + :param size: the requested maximum size. This is either a number to use + for both width and height, or an object with {'width': (width), + 'height': height} in pixels. If None, the default max size is used. + :param maxDefault: a default value to use for width and height. + :returns: maxWidth, maxHeight in pixels. 0 means no images are allowed. + """ + maxWidth = maxHeight = maxDefault + if size is not None: + if isinstance(size, dict): + maxWidth = size.get('width', maxWidth) + maxHeight = size.get('height', maxHeight) + else: + maxWidth = maxHeight = size + # We may want to put an upper limit on what is requested so it can't be + # completely overridden. + return maxWidth, maxHeight
+ + + +
+[docs] +class PILFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to single image PIL files. + """ + + cacheName = 'tilesource' + name = 'pil' + + # Although PIL is mostly a fallback source, prefer it to other fallback + # sources + extensions = { + None: SourcePriority.FALLBACK_HIGH, + 'jpg': SourcePriority.LOW, + 'jpeg': SourcePriority.LOW, + 'jpe': SourcePriority.LOW, + } + mimeTypes = { + None: SourcePriority.FALLBACK_HIGH, + 'image/jpeg': SourcePriority.LOW, + } + + def __init__(self, path, maxSize=None, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: the associated file path. + :param maxSize: either a number or an object with {'width': (width), + 'height': height} in pixels. If None, the default max size is + used. + """ + super().__init__(path, **kwargs) + self.addKnownExtensions() + + self._maxSize = maxSize + if isinstance(maxSize, str): + try: + maxSize = json.loads(maxSize) + except Exception: + msg = ('maxSize must be None, an integer, a dictionary, or a ' + 'JSON string that converts to one of those.') + raise TileSourceError(msg) + self.maxSize = maxSize + + largeImagePath = self._getLargeImagePath() + # Some formats shouldn't be read this way, even if they could. For + # instances, mirax (mrxs) files look like JPEGs, but opening them as + # such misses most of the data. + config._ignoreSourceNames('pil', largeImagePath) + + self._pilImage = None + self._fromRawpy(largeImagePath) + if self._pilImage is None: + try: + self._pilImage = PIL.Image.open(largeImagePath) + except OSError: + if not os.path.isfile(largeImagePath): + raise TileSourceFileNotFoundError(largeImagePath) from None + msg = 'File cannot be opened via PIL.' + raise TileSourceError(msg) + minwh = min(self._pilImage.width, self._pilImage.height) + maxwh = max(self._pilImage.width, self._pilImage.height) + # Throw an exception if too small or big before processing further + if minwh <= 0: + msg = 'PIL tile size is invalid.' + raise TileSourceError(msg) + maxWidth, maxHeight = getMaxSize(maxSize, self.defaultMaxSize()) + if maxwh > max(maxWidth, maxHeight): + msg = 'PIL tile size is too large.' + raise TileSourceError(msg) + self._checkForFrames() + if self._pilImage.info.get('icc_profile', None): + self._iccprofiles = [self._pilImage.info.get('icc_profile')] + # If the rotation flag exists, loading the image may change the width + # and height + if getattr(self._pilImage, '_tile_orientation', None) not in {None, 1}: + self._pilImage.load() + # If this is encoded as a 32-bit integer or a 32-bit float, convert it + # to an 8-bit integer. This expects the source value to either have a + # maximum of 1, 2^8-1, 2^16-1, 2^24-1, or 2^32-1, and scales it to + # [0, 255] + pilImageMode = self._pilImage.mode.split(';')[0] + self._factor = None + if pilImageMode in ('I', 'F'): + imgdata = np.asarray(self._pilImage) + maxval = 256 ** math.ceil(math.log(np.max(imgdata) + 1, 256)) - 1 + self._factor = 255.0 / maxval + self._pilImage = PIL.Image.fromarray(np.uint8(np.multiply( + imgdata, self._factor))) + self.sizeX = self._pilImage.width + self.sizeY = self._pilImage.height + # We have just one tile which is the entire image. + self.tileWidth = self.sizeX + self.tileHeight = self.sizeY + self.levels = 1 + # Throw an exception if too big after processing + if self.tileWidth > maxWidth or self.tileHeight > maxHeight: + msg = 'PIL tile size is too large.' + raise TileSourceError(msg) + + def _checkForFrames(self): + self._frames = None + self._frameCount = 1 + if hasattr(self._pilImage, 'seek'): + baseSize, baseMode = self._pilImage.size, self._pilImage.mode + self._frames = [ + idx for idx, frame in enumerate(PIL.ImageSequence.Iterator(self._pilImage)) + if frame.size == baseSize and frame.mode == baseMode] + self._pilImage.seek(0) + self._frameImage = self._pilImage + self._frameCount = len(self._frames) + self._tileLock = threading.RLock() + + def _fromRawpy(self, largeImagePath): + """ + Try to use rawpy to read an image. + """ + # if rawpy is present, try reading via that library first + try: + import rawpy + + with contextlib.redirect_stderr(open(os.devnull, 'w')): + rgb = rawpy.imread(largeImagePath).postprocess() + rgb = large_image.tilesource.utilities._imageToNumpy(rgb)[0] + if rgb.shape[2] == 2: + rgb = rgb[:, :, :1] + elif rgb.shape[2] > 3: + rgb = rgb[:, :, :3] + self._pilImage = PIL.Image.fromarray( + rgb.astype(np.uint8) if rgb.dtype != np.uint16 else rgb, + ('RGB' if rgb.dtype != np.uint16 else 'RGB;16') if rgb.shape[2] == 3 else + ('L' if rgb.dtype != np.uint16 else 'L;16')) + except Exception: + pass + +
+[docs] + def defaultMaxSize(self): + """ + Get the default max size from the config settings. + + :returns: the default max size. + """ + return int(config.getConfig('max_small_image_size', 4096))
+ + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs): + return strhash( + super(PILFileTileSource, PILFileTileSource).getLRUHash( + *args, **kwargs), + kwargs.get('maxSize'))
+ + +
+[docs] + def getState(self): + return super().getState() + ',' + str( + self._maxSize)
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + result = super().getMetadata() + if getattr(self, '_frames', None) is not None and len(self._frames) > 1: + result['frames'] = [{} for idx in range(len(self._frames))] + self._addMetadataFrameInformation(result) + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + results = {'pil': {}} + for key in ('format', 'mode', 'size', 'width', 'height', 'palette', 'info'): + try: + results['pil'][key] = getattr(self._pilImage, key) + except Exception: + pass + return results
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, + mayRedirect=False, **kwargs): + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame, self._frameCount) + if frame != 0: + with self._tileLock: + self._frameImage.seek(self._frames[frame]) + try: + img = self._frameImage.copy() + except Exception: + pass + self._frameImage.seek(0) + img.load() + if self._factor: + img = PIL.Image.fromarray(np.uint8(np.multiply( + np.asarray(img), self._factor))) + else: + img = self._pilImage + return self._outputTile(img, TILE_FORMAT_PIL, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+ + +
+[docs] + @classmethod + def addKnownExtensions(cls): + if not hasattr(cls, '_addedExtensions'): + cls._addedExtensions = True + cls.extensions = cls.extensions.copy() + cls.mimeTypes = cls.mimeTypes.copy() + for dotext in PIL.Image.registered_extensions(): + ext = dotext.lstrip('.') + if ext not in cls.extensions: + cls.extensions[ext] = SourcePriority.IMPLICIT_HIGH + for mimeType in PIL.Image.MIME.values(): + if mimeType not in cls.mimeTypes: + cls.mimeTypes[mimeType] = SourcePriority.IMPLICIT_HIGH
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return PILFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return PILFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_pil/girder_source.html b/_modules/large_image_source_pil/girder_source.html new file mode 100644 index 000000000..fb79e29d2 --- /dev/null +++ b/_modules/large_image_source_pil/girder_source.html @@ -0,0 +1,205 @@ + + + + + + large_image_source_pil.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_pil.girder_source

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import cherrypy
+from girder_large_image.constants import PluginSettings
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from girder.models.setting import Setting
+from large_image.cache_util import methodcache
+from large_image.constants import TILE_FORMAT_PIL
+from large_image.exceptions import TileSourceError
+
+from . import PILFileTileSource
+
+
+
+[docs] +class PILGirderTileSource(PILFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with a PIL file. + """ + + # Cache size is based on what the class needs, which does not include + # individual tiles + cacheName = 'tilesource' + name = 'pil' + +
+[docs] + def defaultMaxSize(self): + return int(Setting().get( + PluginSettings.LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE))
+ + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs): + return GirderTileSource.getLRUHash(*args, **kwargs) + ',%s' % (str( + kwargs.get('maxSize', args[1] if len(args) >= 2 else None)))
+ + +
+[docs] + def getState(self): + return super().getState() + ',' + str( + self._maxSize)
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, + mayRedirect=False, **kwargs): + if z != 0: + msg = 'z layer does not exist' + raise TileSourceError(msg) + if x != 0: + msg = 'x is outside layer' + raise TileSourceError(msg) + if y != 0: + msg = 'y is outside layer' + raise TileSourceError(msg) + if (mayRedirect and not pilImageAllowed and not numpyAllowed and + cherrypy.request and + self._pilFormatMatches(self._pilImage, mayRedirect, **kwargs)): + url = '%s/api/v1/file/%s/download' % ( + cherrypy.request.base, self.item['largeImage']['fileId']) + raise cherrypy.HTTPRedirect(url) + return self._outputTile(self._pilImage, TILE_FORMAT_PIL, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_rasterio.html b/_modules/large_image_source_rasterio.html new file mode 100644 index 000000000..aa5cd3546 --- /dev/null +++ b/_modules/large_image_source_rasterio.html @@ -0,0 +1,1254 @@ + + + + + + large_image_source_rasterio — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_rasterio

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import math
+import os
+import pathlib
+import tempfile
+import threading
+import warnings
+from contextlib import suppress
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import numpy as np
+import PIL.Image
+
+import large_image
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import (TILE_FORMAT_IMAGE, TILE_FORMAT_NUMPY,
+                                   TILE_FORMAT_PIL, SourcePriority,
+                                   TileInputUnits, TileOutputMimeTypes)
+from large_image.exceptions import (TileSourceError,
+                                    TileSourceFileNotFoundError,
+                                    TileSourceInefficientError)
+from large_image.tilesource.geo import (GDALBaseFileTileSource,
+                                        ProjUnitsAcrossLevel0,
+                                        ProjUnitsAcrossLevel0_MaxSize)
+from large_image.tilesource.utilities import JSONDict
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+rio = None
+Affine = None
+
+
+def _lazyImport():
+    """
+    Import the rasterio module.  This is done when needed rather than in the
+    module initialization because it is slow.
+    """
+    global Affine, rio
+
+    if rio is None:
+        try:
+            import affine
+            import rasterio as rio
+            import rasterio.warp
+
+            Affine = affine.Affine
+
+            warnings.filterwarnings(
+                'ignore', category=rio.errors.NotGeoreferencedWarning, module='rasterio')
+            rio._env.code_map.pop(1, None)
+        except ImportError:
+            msg = 'rasterio module not found.'
+            raise TileSourceError(msg)
+
+
+
+[docs] +def make_crs(projection): + _lazyImport() + + if isinstance(projection, str): + return rio.CRS.from_string(projection) + if isinstance(projection, dict): + return rio.CRS.from_dict(projection) + if isinstance(projection, int): + return rio.CRS.from_string(f'EPSG:{projection}') + return rio.CRS(projection)
+ + + +
+[docs] +class RasterioFileTileSource(GDALBaseFileTileSource, metaclass=LruCacheMetaclass): + """Provides tile access to geospatial files.""" + + cacheName = 'tilesource' + name = 'rasterio' + + def __init__(self, path, projection=None, unitsPerPixel=None, **kwargs): + """Initialize the tile class. + + See the base class for other available parameters. + + :param path: a filesystem path for the tile source. + :param projection: None to use pixel space, otherwise a crs compatible with rasterio's CRS. + :param unitsPerPixel: The size of a pixel at the 0 tile size. + Ignored if the projection is None. For projections, None uses the default, + which is the distance between (-180,0) and (180,0) in EPSG:4326 converted to the + projection divided by the tile size. crs projections that are not latlong + (is_geographic is False) must specify unitsPerPixel. + + """ + # init the object + super().__init__(path, **kwargs) + _lazyImport() + self.addKnownExtensions() + + # create a thread lock + self._getDatasetLock = threading.RLock() + + if isinstance(path, rio.io.MemoryFile): + path = path.open(mode='r') + + if isinstance(path, rio.io.DatasetReaderBase): + self.dataset = path + self._largeImagePath = self.dataset.name + else: + # set the large_image path + self._largeImagePath = self._getLargeImagePath() + + # open the file with rasterio and display potential warning/errors + with self._getDatasetLock: + if not self._largeImagePath.startswith( + '/vsi') and not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + try: + self.dataset = rio.open(self._largeImagePath) + except rio.errors.RasterioIOError: + msg = 'File cannot be opened via rasterio.' + raise TileSourceError(msg) + if self.dataset.driver == 'netCDF': + msg = 'netCDF file will not be read via rasterio source.' + raise TileSourceError(msg) + + # extract default parameters from the image + self.tileSize = 256 + self._bounds = {} + self.tileWidth = self.tileSize + self.tileHeight = self.tileSize + self.projection = make_crs(projection) if projection else None + + # get width and height parameters + with self._getDatasetLock: + self.sourceSizeX = self.sizeX = self.dataset.width + self.sourceSizeY = self.sizeY = self.dataset.height + + # netCDF is blacklisted from rasterio so it won't be used. + # use the mapnik source if needed. This variable is always ignored + # is_netcdf = False + + # get the different scales and projections from the image + scale = self.getPixelSizeInMeters() + + # raise an error if we are missing some information about the projection + # i.e. we don't know where to place it on a map + isProjected = self.projection or self.dataset.driver.lower() in {'png'} + if isProjected and not scale: + msg = ('File does not have a projected scale, so will not be ' + 'opened via rasterio with a projection.') + raise TileSourceError(msg) + + # set the levels of the tiles + logX = math.log(float(self.sizeX) / self.tileWidth) + logY = math.log(float(self.sizeY) / self.tileHeight) + computedLevel = math.ceil(max(logX, logY) / math.log(2)) + self.sourceLevels = self.levels = int(max(0, computedLevel) + 1) + + self._unitsPerPixel = unitsPerPixel + self.projection is None or self._initWithProjection(unitsPerPixel) + self._getPopulatedLevels() + self._getTileLock = threading.Lock() + self._setDefaultStyle() + + def _getPopulatedLevels(self): + try: + with self._getDatasetLock: + self._populatedLevels = 1 + len(self.dataset.overviews(1)) + except Exception: + pass + + def _scanForMinMax(self, dtype, frame=0, analysisSize=1024, onlyMinMax=True): + """Update the band range of the data type to the end of the range list. + + This will change autocalling behavior, and for non-integer data types, + this adds the range [0, 1]. + + :param dtype: the dtype of the bands + :param frame: optional default to 0 + :param analysisSize: optional default to 1024 + :param onlyMinMax: optional default to True + """ + # default frame to 0 in case it is set to None from outside + frame = frame or 0 + + # read band information + bandInfo = self.getBandInformation() + + # get the minmax value from the band + hasMin = all(b.get('min') is not None for b in bandInfo.values()) + hasMax = all(b.get('max') is not None for b in bandInfo.values()) + if not frame and onlyMinMax and hasMax and hasMin: + with self._getDatasetLock: + dtype = self.dataset.profile['dtype'] + self._bandRanges[0] = { + 'min': np.array([b['min'] for b in bandInfo.values()], dtype=dtype), + 'max': np.array([b['max'] for b in bandInfo.values()], dtype=dtype), + } + else: + kwargs = {} + if self.projection: + bounds = self.getBounds(self.projection) + kwargs = { + 'region': { + 'left': bounds['xmin'], + 'top': bounds['ymax'], + 'right': bounds['xmax'], + 'bottom': bounds['ymin'], + 'units': 'projection', + }, + } + super(RasterioFileTileSource, RasterioFileTileSource)._scanForMinMax( + self, + dtype=dtype, + frame=frame, + analysisSize=analysisSize, + onlyMinMax=onlyMinMax, + **kwargs, + ) + + # Add the maximum range of the data type to the end of the band + # range list. This changes autoscaling behavior. For non-integer + # data types, this adds the range [0, 1]. + band_frame = self._bandRanges[frame] + try: + # only valid for integer dtypes + range_max = np.iinfo(band_frame['max'].dtype).max + except ValueError: + range_max = 1 + band_frame['min'] = np.append(band_frame['min'], 0) + band_frame['max'] = np.append(band_frame['max'], range_max) + + def _initWithProjection(self, unitsPerPixel=None): + """Initialize aspects of the class when a projection is set. + + :param unitsPerPixel: optional default to None + """ + srcCrs = make_crs(4326) + # Since we already converted to bytes decoding is safe here + dstCrs = self.projection + if dstCrs.is_geographic: + msg = ('Projection must not be geographic (it needs to use linear ' + 'units, not longitude/latitude).') + raise TileSourceError(msg) + + if unitsPerPixel is not None: + self.unitsAcrossLevel0 = float(unitsPerPixel) * self.tileSize + else: + self.unitsAcrossLevel0 = ProjUnitsAcrossLevel0.get( + self.projection.to_string(), + ) + if self.unitsAcrossLevel0 is None: + # If unitsPerPixel is not specified, the horizontal distance + # between -180,0 and +180,0 is used. Some projections (such as + # stereographic) will fail in this case; they must have a unitsPerPixel specified. + east, _ = rio.warp.transform(srcCrs, dstCrs, [-180], [0]) + west, _ = rio.warp.transform(srcCrs, dstCrs, [180], [0]) + self.unitsAcrossLevel0 = abs(east[0] - west[0]) + if not self.unitsAcrossLevel0: + msg = 'unitsPerPixel must be specified for this projection' + raise TileSourceError(msg) + if len(ProjUnitsAcrossLevel0) >= ProjUnitsAcrossLevel0_MaxSize: + ProjUnitsAcrossLevel0.clear() + + ProjUnitsAcrossLevel0[ + self.projection.to_string() + ] = self.unitsAcrossLevel0 + + # for consistency, it should probably always be (0, 0). Whatever + # renders the map would need the same offset as used here. + self.projectionOrigin = (0, 0) + + # Calculate values for this projection + width = self.getPixelSizeInMeters() * self.tileWidth + tile0 = self.unitsAcrossLevel0 / width + base2 = math.ceil(math.log(tile0) / math.log(2)) + self.levels = int(max(int(base2) + 1, 1)) + + # Report sizeX and sizeY as the whole world + self.sizeX = 2 ** (self.levels - 1) * self.tileWidth + self.sizeY = 2 ** (self.levels - 1) * self.tileHeight + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs): + projection = kwargs.get('projection', args[1] if len(args) >= 2 else None) + unitsPerPixel = kwargs.get('unitsPerPixel', args[3] if len(args) >= 4 else None) + + source = super(RasterioFileTileSource, RasterioFileTileSource) + lru = source.getLRUHash(*args, **kwargs) + info = f',{projection},{unitsPerPixel}' + + return lru + info
+ + +
+[docs] + def getState(self): + proj = self.projection.to_string() if self.projection else None + unit = self._unitsPerPixel + + return super().getState() + f',{proj},{unit}'
+ + +
+[docs] + def getCrs(self): + """Returns crs object for the given dataset + + :returns: The crs or None. + """ + with self._getDatasetLock: + + # use gcp if available + if len(self.dataset.gcps[0]) != 0 and self.dataset.gcps[1]: + crs = self.dataset.gcps[1] + else: + crs = self.dataset.crs + + # if no crs but the file is a NITF or has a valid affine transform then + # consider it as 4326 + hasTransform = self.dataset.transform != Affine.identity() + isNitf = self.dataset.driver.lower() in {'NITF'} + if not crs and (hasTransform or isNitf): + crs = make_crs(4326) + + return crs
+ + + def _getAffine(self): + """Get the Affine transformation. + + If GCPs are used, get the appropriate Affine for those. Be careful, + Rasterio have deprecated GDAL styled transform in favor + of ``Affine`` objects. See their documentation for more information: + shorturl.at/bcdGL + + :returns: a six-component array with the transform + """ + with self._getDatasetLock: + affine = self.dataset.transform + if len(self.dataset.gcps[0]) != 0 and self.dataset.gcps[1]: + affine = rio.transform.from_gcps(self.dataset.gcps[0]) + + return affine + +
+[docs] + def getBounds(self, crs=None, **kwargs): + """Returns bounds of the image. + + :param crs: the projection for the bounds. None for the default. + + :returns: an object with the four corners and the projection that was used. + None if we don't know the original projection. + """ + if crs is None and 'srs' in kwargs: + crs = kwargs.get('srs') + + # read the crs as a crs if needed + dstCrs = make_crs(crs) if crs else None + strDstCrs = 'none' if dstCrs is None else dstCrs.to_string() + + # exit if it's already set + if strDstCrs in self._bounds: + return self._bounds[strDstCrs] + + # extract the projection information + af = self._getAffine() + srcCrs = self.getCrs() + + # set bounds to none and exit if no crs is set for the dataset + if not srcCrs: + self._bounds[strDstCrs] = None + return + + # compute the corner coordinates using the affine transformation as + # longitudes and latitudes. Cannot only rely on bounds because of + # rotated coordinate systems + bounds = { + 'll': { + 'x': af[2] + self.sourceSizeY * af[1], + 'y': af[5] + self.sourceSizeY * af[4], + }, + 'ul': { + 'x': af[2], + 'y': af[5], + }, + 'lr': { + 'x': af[2] + self.sourceSizeX * af[0] + self.sourceSizeY * af[1], + 'y': af[5] + self.sourceSizeX * af[3] + self.sourceSizeY * af[4], + }, + 'ur': { + 'x': af[2] + self.sourceSizeX * af[0], + 'y': af[5] + self.sourceSizeX * af[3], + }, + } + + # ensure that the coordinates are within the projection limits + if srcCrs.is_geographic and dstCrs: + + # set the vertical bounds + # some projection system don't cover the poles so we need to adapt + # the values of ybounds accordingly + has_poles = rio.warp.transform(4326, dstCrs, [0], [90])[1][0] != float('inf') + yBounds = 90 if has_poles else 89.999999 + + # for each corner fix the latitude within -yBounds yBounds + for k in bounds: + bounds[k]['y'] = max(min(bounds[k]['y'], yBounds), -yBounds) + + # for each corner rotate longitude until it's within -180, 180 + while any(v['x'] > 180 for v in bounds.values()): + for k in bounds: + bounds[k]['x'] -= 180 + while any(v['x'] < -180 for v in bounds.values()): + for k in bounds: + bounds[k]['x'] += 360 + + # if one of the corner is > 180 set all the corner to world width + if any(v['x'] >= 180 for v in bounds.values()): + bounds['ul']['x'] = bounds['ll']['x'] = -180 + bounds['ur']['x'] = bounds['lr']['x'] = 180 + + # reproject the pts in the destination coordinate system if necessary + needProjection = dstCrs and dstCrs != srcCrs + if needProjection: + for pt in bounds.values(): + [pt['x']], [pt['y']] = rio.warp.transform(srcCrs, dstCrs, [pt['x']], [pt['y']]) + + # extract min max coordinates from the corners + ll = bounds['ll']['x'], bounds['ll']['y'] + ul = bounds['ul']['x'], bounds['ul']['y'] + lr = bounds['lr']['x'], bounds['lr']['y'] + ur = bounds['ur']['x'], bounds['ur']['y'] + bounds['xmin'] = min(ll[0], ul[0], lr[0], ur[0]) + bounds['xmax'] = max(ll[0], ul[0], lr[0], ur[0]) + bounds['ymin'] = min(ll[1], ul[1], lr[1], ur[1]) + bounds['ymax'] = max(ll[1], ul[1], lr[1], ur[1]) + + # set the srs in the bounds + bounds['srs'] = dstCrs.to_string() if needProjection else srcCrs.to_string() + + # write the bounds in memory + self._bounds[strDstCrs] = bounds + + return bounds
+ + +
+[docs] + def getBandInformation(self, statistics=True, dataset=None, **kwargs): + """Get information about each band in the image. + + :param statistics: if True, compute statistics if they don't already exist. + Ignored: always treated as True. + :param dataset: the dataset. If None, use the main dataset. + + :returns: a list of one dictionary per band. Each dictionary contains + known values such as interpretation, min, max, mean, stdev, nodata, + scale, offset, units, categories, colortable, maskband. + """ + # exit if the value is already set + if getattr(self, '_bandInfo', None) and not dataset: + return self._bandInfo + + # check if the dataset is cached + cache = not dataset + + # do everything inside the dataset lock to avoid multiple read + with self._getDatasetLock: + + # setup the dataset (use the one store in self.dataset if not cached) + dataset = dataset or self.dataset + + # loop in the bands to get the indicidative stats (bands are 1 indexed) + infoSet = JSONDict({}) + for i in dataset.indexes: # 1 indexed + + # get the stats + stats = dataset.statistics(i, approx=True, clear_cache=True) + + # rasterio doesn't provide support for maskband as for RCF 15 + # instead the whole mask numpy array is rendered. We don't want to save it + # in the metadata + info = { + 'min': stats.min, + 'max': stats.max, + 'mean': stats.mean, + 'stdev': stats.std, + 'nodata': dataset.nodatavals[i - 1], + 'scale': dataset.scales[i - 1], + 'offset': dataset.offsets[i - 1], + 'units': dataset.units[i - 1], + 'categories': dataset.descriptions[i - 1], + 'interpretation': dataset.colorinterp[i - 1].name.lower(), + } + if info['interpretation'] == 'palette': + info['colortable'] = list(dataset.colormap(i).values()) + # if dataset.mask_flag_enums[i - 1][0] != MaskFlags.all_valid: + # # TODO: find band number - this is incorrect + # info["maskband"] = dataset.mask_flag_enums[i - 1][1].value + + # Only keep values that aren't None or the empty string + infoSet[i] = { + k: v for k, v in info.items() + if v not in (None, '') and not ( + isinstance(v, float) and + math.isnan(v) + ) + } + + # set the value to cache if needed + if cache: + self._bandInfo = infoSet + + return infoSet
+ + +
+[docs] + def getMetadata(self): + metadata = super().getMetadata() + with self._getDatasetLock: + # check if the file is geospatial + has_projection = self.dataset.crs + has_gcps = len(self.dataset.gcps[0]) != 0 and self.dataset.gcps[1] + has_affine = self.dataset.transform + + metadata.update({ + 'geospatial': bool(has_projection or has_gcps or has_affine), + 'sourceLevels': self.sourceLevels, + 'sourceSizeX': self.sourceSizeX, + 'sourceSizeY': self.sourceSizeY, + 'bounds': self.getBounds(self.projection), + 'projection': self.projection.decode() if isinstance( + self.projection, bytes) else self.projection, + 'sourceBounds': self.getBounds(), + 'bands': self.getBandInformation(), + }) + return metadata
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """Return additional known metadata about the tile source. + + Data returned from this method is not guaranteed to be in + any particular format or have specific values. + + :returns: a dictionary of data or None. + """ + result = JSONDict({}) + with self._getDatasetLock: + result['driverShortName'] = self.dataset.driver + result['driverLongName'] = self.dataset.driver + # result['fileList'] = self.dataset.GetFileList() + result['RasterXSize'] = self.dataset.width + result['RasterYSize'] = self.dataset.height + result['Affine'] = self._getAffine() + result['Projection'] = ( + self.dataset.crs.to_string() if self.dataset.crs else None + ) + result['GCPProjection'] = self.dataset.gcps[1] + + meta = self.dataset.meta + meta['crs'] = ( + meta['crs'].to_string() + if ('crs' in meta and meta['crs'] is not None) + else None + ) + meta['transform'] = ( + meta['transform'].to_gdal() if 'transform' in meta else None + ) + result['Metadata'] = meta + + # add gcp of available + if len(self.dataset.gcps[0]) != 0: + result['GCPs'] = [gcp.asdict() for gcp in self.dataset.gcps[0]] + + return result
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + if not self.projection: + self._xyzInRange(x, y, z) + factor = int(2 ** (self.levels - 1 - z)) + xmin = int(x * factor * self.tileWidth) + ymin = int(y * factor * self.tileHeight) + xmax = int(min(xmin + factor * self.tileWidth, self.sourceSizeX)) + ymax = int(min(ymin + factor * self.tileHeight, self.sourceSizeY)) + w = int(max(1, round((xmax - xmin) / factor))) + h = int(max(1, round((ymax - ymin) / factor))) + + with self._getDatasetLock: + window = rio.windows.Window(xmin, ymin, xmax - xmin, ymax - ymin) + count = self.dataset.count + tile = self.dataset.read( + window=window, + out_shape=(count, h, w), + resampling=rio.enums.Resampling.nearest, + ) + + else: + xmin, ymin, xmax, ymax = self.getTileCorners(z, x, y) + bounds = self.getBounds(self.projection) + + # return empty image when I'm out of bounds + if ( + xmin >= bounds['xmax'] or + xmax <= bounds['xmin'] or + ymin >= bounds['ymax'] or + ymax <= bounds['ymin'] + ): + pilimg = PIL.Image.new('RGBA', (self.tileWidth, self.tileHeight)) + return self._outputTile( + pilimg, TILE_FORMAT_PIL, x, y, z, applyStyle=False, **kwargs, + ) + + xres = (xmax - xmin) / self.tileWidth + yres = (ymax - ymin) / self.tileHeight + dst_transform = Affine(xres, 0.0, xmin, 0.0, -yres, ymax) + + # Adding an alpha band when the source has one is trouble. + # It will result in surprisingly unmasked data. + src_alpha_band = 0 + for i, interp in enumerate(self.dataset.colorinterp): + if interp == rio.enums.ColorInterp.alpha: + src_alpha_band = i + add_alpha = not src_alpha_band + + # read the image as a warp vrt + with self._getDatasetLock: + with rio.vrt.WarpedVRT( + self.dataset, + resampling=rio.enums.Resampling.nearest, + crs=self.projection, + transform=dst_transform, + height=self.tileHeight, + width=self.tileWidth, + add_alpha=add_alpha, + ) as vrt: + tile = vrt.read(resampling=rio.enums.Resampling.nearest) + + # necessary for multispectral images: + # set the coordinates first and the bands at the end + if len(tile.shape) == 3: + tile = np.moveaxis(tile, 0, 2) + + return self._outputTile( + tile, TILE_FORMAT_NUMPY, x, y, z, pilImageAllowed, numpyAllowed, **kwargs, + )
+ + + def _convertProjectionUnits( + self, left, top, right, bottom, width=None, height=None, units='base_pixels', **kwargs, + ): + """Convert projection units. + + Given bound information and a units that consists of a projection (srs or crs), + convert the bounds to either pixel or the class projection coordinates. + + :param left: the left edge (inclusive) of the region to process. + :param top: the top edge (inclusive) of the region to process. + :param right: the right edge (exclusive) of the region to process. + :param bottom: the bottom edge (exclusive) of the region to process. + :param width: the width of the region to process. Ignored if both left and + right are specified. + :param height: the height of the region to process. Ignores if both top and + bottom are specified. + :param units: either 'projection', a string starting with 'proj4:','epsg:', + or '+proj=' or a enumerated value like 'wgs84', or one of the super's values. + :param kwargs: optional parameters. + + :returns: left, top, right, bottom, units. The new bounds in the either + pixel or class projection units. + """ + # build the different corner from the parameters + if not kwargs.get('unitsWH') or kwargs.get('unitsWH') == units: + if left is None and right is not None and width is not None: + left = right - width + if right is None and left is not None and width is not None: + right = left + width + if top is None and bottom is not None and height is not None: + top = bottom - height + if bottom is None and top is not None and height is not None: + bottom = top + height + + # raise error if we didn't build one of the coordinates + if (left is None and right is None) or (top is None and bottom is None): + msg = ('Cannot convert from projection unless at least one of ' + 'left and right and at least one of top and bottom is ' + 'specified.') + raise TileSourceError(msg) + + # compute the pixel coordinates of the corners if no projection is set + if not self.projection: + pleft, ptop = self.toNativePixelCoordinates( + right if left is None else left, bottom if top is None else top, units, + ) + pright, pbottom = self.toNativePixelCoordinates( + left if right is None else right, + top if bottom is None else bottom, + units, + ) + units = 'base_pixels' + + # compute the coordinates if the projection exist + else: + if units.startswith('proj4:'): + # HACK to avoid `proj4:` prefixes with `WGS84`, etc. + units = units.split(':', 1)[1] + srcCrs = make_crs(units) + dstCrs = self.projection # instance projection -- do not use the CRS native to the file + [pleft], [ptop] = rio.warp.transform( + srcCrs, dstCrs, + [right if left is None else left], + [bottom if top is None else top]) + [pright], [pbottom] = rio.warp.transform( + srcCrs, dstCrs, + [left if right is None else right], + [top if bottom is None else bottom]) + units = 'projection' + + # set the corner value in pixel coordinates if the coordinate was initially + # set else leave it to None + left = pleft if left is not None else None + top = ptop if top is not None else None + right = pright if right is not None else None + bottom = pbottom if bottom is not None else None + + return left, top, right, bottom, units + + def _getRegionBounds( + self, + metadata, + left=None, + top=None, + right=None, + bottom=None, + width=None, + height=None, + units=None, + **kwargs, + ): + """Get region bounds. + + Given a set of arguments that can include left, right, top, bottom, width, + height, and units, generate actual pixel values for left, top, right, and bottom. + If units is `'projection'`, use the source's projection. If units is a + proj string, use that projection. Otherwise, just use the super function. + + :param metadata: the metadata associated with this source. + :param left: the left edge (inclusive) of the region to process. + :param top: the top edge (inclusive) of the region to process. + :param right: the right edge (exclusive) of the region to process. + :param bottom: the bottom edge (exclusive) of the region to process. + :param width: the width of the region to process. Ignored if both left and + right are specified. + :param height: the height of the region to process. Ignores if both top and + bottom are specified. + :param units: either 'projection', a string starting with 'proj4:', 'epsg:' + or a enumarted value like 'wgs84', or one of the super's values. + :param kwargs: optional parameters from _convertProjectionUnits. See above. + + :returns: left, top, right, bottom bounds in pixels. + """ + isUnits = units is not None + units = TileInputUnits.get(units.lower() if isUnits else None, units) + + # check if the units is a string or projection material + isProj = False + with suppress(rio.errors.CRSError): + isProj = make_crs(units) is not None + + # convert the coordinates if a projection exist + if isUnits and isProj: + left, top, right, bottom, units = self._convertProjectionUnits( + left, top, right, bottom, width, height, units, **kwargs, + ) + + if units == 'projection' and self.projection: + bounds = self.getBounds(self.projection) + + # Fill in missing values + if left is None: + left = bounds['xmin'] if right is None or width is None else right - \ + width # fmt: skip + if right is None: + right = bounds['xmax'] if width is None else left + width + if top is None: + top = bounds['ymax'] if bottom is None or height is None else bottom - \ + height # fmt: skip + if bottom is None: + bottom = bounds['ymin'] if height is None else top + height + + # remove width and height if necessary + if not kwargs.get('unitsWH') or kwargs.get('unitsWH') == units: + width = height = None + + # Convert to [-0.5, 0.5], [-0.5, 0.5] coordinate range + left = (left - self.projectionOrigin[0]) / self.unitsAcrossLevel0 + right = (right - self.projectionOrigin[0]) / self.unitsAcrossLevel0 + top = (top - self.projectionOrigin[1]) / self.unitsAcrossLevel0 + bottom = (bottom - self.projectionOrigin[1]) / self.unitsAcrossLevel0 + + # Convert to worldwide 'base pixels' and crop to the world + xScale = 2 ** (self.levels - 1) * self.tileWidth + yScale = 2 ** (self.levels - 1) * self.tileHeight + left = max(0, min(xScale, (0.5 + left) * xScale)) + right = max(0, min(xScale, (0.5 + right) * xScale)) + top = max(0, min(yScale, (0.5 - top) * yScale)) + bottom = max(0, min(yScale, (0.5 - bottom) * yScale)) + + # Ensure correct ordering + left, right = min(left, right), max(left, right) + top, bottom = min(top, bottom), max(top, bottom) + units = 'base_pixels' + + return super()._getRegionBounds( + metadata, left, top, right, bottom, width, height, units, **kwargs, + ) + +
+[docs] + def pixelToProjection(self, x, y, level=None): + """Convert from pixels back to projection coordinates. + + :param x, y: base pixel coordinates. + :param level: the level of the pixel. None for maximum level. + + :returns: px, py in projection coordinates. + """ + if level is None: + level = self.levels - 1 + + # if no projection is set build the pixel values using the geotransform + if not self.projection: + af = self._getAffine() + x *= 2 ** (self.levels - 1 - level) + y *= 2 ** (self.levels - 1 - level) + x = af[2] + af[0] * x + af[1] * y + y = af[5] + af[3] * x + af[4] * y + + # else we used the projection set in __init__ + else: + xScale = 2**level * self.tileWidth + yScale = 2**level * self.tileHeight + x = x / xScale - 0.5 + y = 0.5 - y / yScale + x = x * self.unitsAcrossLevel0 + self.projectionOrigin[0] + y = y * self.unitsAcrossLevel0 + self.projectionOrigin[1] + + return x, y
+ + +
+[docs] + def toNativePixelCoordinates(self, x, y, crs=None, roundResults=True): + """Convert a coordinate in the native projection to pixel coordinates. + + :param x: the x coordinate it the native projection. + :param y: the y coordinate it the native projection. + :param crs: input projection. None to use the sources's projection. + :param roundResults: if True, round the results to the nearest pixel. + + :return: (x, y) the pixel coordinate. + """ + srcCrs = self.projection if crs is None else make_crs(crs) + + # convert to the native projection + dstCrs = make_crs(self.getCrs()) + [px], [py] = rio.warp.transform(srcCrs, dstCrs, [x], [y]) + + # convert to native pixel coordinates + af = self._getAffine() + d = af[1] * af[3] - af[0] * af[4] + x = (af[2] * af[4] - af[1] * af[5] - af[4] * px + af[1] * py) / d + y = (af[0] * af[5] - af[2] * af[3] + af[3] * px - af[0] * py) / d + + # convert to integer if requested + if roundResults: + x, y = int(round(x)), int(round(y)) + + return x, y
+ + +
+[docs] + def getPixel(self, **kwargs): + """Get a single pixel from the current tile source. + + :param kwargs: optional arguments. Some options are region, output, encoding, + jpegQuality, jpegSubsampling, tiffCompression, fill. See tileIterator. + + :returns: a dictionary with the value of the pixel for each channel on a + scale of [0-255], including alpha, if available. This may contain + additional information. + """ + pixel = super().getPixel(includeTileRecord=True, **kwargs) + tile = pixel.pop('tile', None) + + if tile: + # Coordinates in the max level tile + x, y = tile['gx'], tile['gy'] + + if self.projection: + # convert to a scale of [-0.5, 0.5] + x = 0.5 + x / 2 ** (self.levels - 1) / self.tileWidth + y = 0.5 - y / 2 ** (self.levels - 1) / self.tileHeight + # convert to projection coordinates + x = self.projectionOrigin[0] + x * self.unitsAcrossLevel0 + y = self.projectionOrigin[1] + y * self.unitsAcrossLevel0 + # convert to native pixel coordinates + x, y = self.toNativePixelCoordinates(x, y) + + if 0 <= int(x) < self.sizeX and 0 <= int(y) < self.sizeY: + with self._getDatasetLock: + for i in self.dataset.indexes: + window = rio.windows.Window(int(x), int(y), 1, 1) + try: + value = self.dataset.read( + i, window=window, resampling=rio.enums.Resampling.nearest, + ) + value = value[0][0] # there should be 1 single pixel + pixel.setdefault('bands', {})[i] = value.item() + except RuntimeError: + pass + return pixel
+ + + def _encodeTiledImageFromVips(self, vimg, iterInfo, image, **kwargs): + raise NotImplementedError + +
+[docs] + def getRegion(self, format=(TILE_FORMAT_IMAGE,), **kwargs): + """Get region. + + Get a rectangular region from the current tile source. Aspect ratio is preserved. + If neither width nor height is given, the original size of the highest + resolution level is used. If both are given, the returned image will be + no larger than either size. + + :param format: the desired format or a tuple of allowed formats. Formats + are members of (TILE_FORMAT_PIL, TILE_FORMAT_NUMPY, TILE_FORMAT_IMAGE). + If TILE_FORMAT_IMAGE, encoding may be specified. + :param kwargs: optional arguments. Some options are region, output, encoding, + jpegQuality, jpegSubsampling, tiffCompression, fill. See tileIterator. + + :returns: regionData, formatOrRegionMime: the image data and either the + mime type, if the format is TILE_FORMAT_IMAGE, or the format. + """ + # cast format as a tuple if needed + format = format if isinstance(format, (tuple, set, list)) else (format,) + + if self.projection is None: + if kwargs.get('encoding') == 'TILED': + msg = 'getRegion() with TILED output can only be used with a projection.' + raise NotImplementedError(msg) + return super().getRegion(format, **kwargs) + + # The tile iterator handles determining the output region + iterInfo = self.tileIterator(format=TILE_FORMAT_NUMPY, resample=None, **kwargs).info + + if not ( + iterInfo and + not self._jsonstyle and + TILE_FORMAT_IMAGE in format and + kwargs.get('encoding') == 'TILED' + ): + return super().getRegion(format, **kwargs) + + left, top = self.pixelToProjection( + iterInfo['region']['left'], iterInfo['region']['top'], iterInfo['level']) + right, bottom = self.pixelToProjection( + iterInfo['region']['right'], iterInfo['region']['bottom'], iterInfo['level']) + # Be sure to use set output size + width = iterInfo['output']['width'] + height = iterInfo['output']['height'] + + with self._getDatasetLock, tempfile.NamedTemporaryFile( + suffix='.tiff', prefix='tiledGeoRegion_', delete=False, + ) as output: + + xres = (right - left) / width + yres = (top - bottom) / height + dst_transform = Affine(xres, 0.0, left, 0.0, -yres, top) + + with rio.vrt.WarpedVRT( + self.dataset, + resampling=rio.enums.Resampling.nearest, + crs=self.projection, + transform=dst_transform, + height=height, + width=width, + ) as vrt: + data = vrt.read(resampling=rio.enums.Resampling.nearest) + + profile = self.dataset.meta.copy() + profile.update( + large_image.tilesource.utilities._rasterioParameters( + defaultCompression='lzw', **kwargs, + ), + ) + profile.update({ + 'crs': self.projection, + 'height': height, + 'width': width, + 'transform': dst_transform, + }) + with rio.open(output.name, 'w', **profile) as dst: + dst.write(data) + # Write colormaps if available + for i in range(data.shape[0]): + if self.dataset.colorinterp[i].name.lower() == 'palette': + dst.write_colormap(i + 1, self.dataset.colormap(i + 1)) + + return pathlib.Path(output.name), TileOutputMimeTypes['TILED']
+ + +
+[docs] + def validateCOG(self, strict=True, warn=True): + """Check if this image is a valid Cloud Optimized GeoTiff. + + This will raise a :class:`large_image.exceptions.TileSourceInefficientError` + if not a valid Cloud Optimized GeoTiff. Otherwise, returns True. Requires + the ``rio-cogeo`` lib. + + + :param strict: Enforce warnings as exceptions. Set to False to only warn + and not raise exceptions. + :param warn: Log any warnings + + :returns: the validity of the cogtiff + """ + try: + from rio_cogeo.cogeo import cog_validate + except ImportError: + msg = 'Please install `rio-cogeo` to check COG validity.' + raise ImportError(msg) + + isValid, errors, warnings = cog_validate(self._largeImagePath, strict=strict) + + if errors: + raise TileSourceInefficientError(errors) + if strict and warnings: + raise TileSourceInefficientError(warnings) + if warn: + for warning in warnings: + self.logger.warning(warning) + + return isValid
+ + +
+[docs] + @staticmethod + def isGeospatial(path): + """ + Check if a path is likely to be a geospatial file. + + :param path: The path to the file + :returns: True if geospatial. + """ + _lazyImport() + + if isinstance(path, rio.io.DatasetReaderBase): + ds = path + else: + try: + ds = rio.open(path) + except Exception: + return False + if ds.crs or (ds.transform and ds.transform != rio.Affine(1, 0, 0, 0, 1, 0)): + return True + if len(ds.gcps[0]) and ds.gcps[1]: + return True + return False
+ + +
+[docs] + @classmethod + def addKnownExtensions(cls): + import rasterio.drivers + + if not hasattr(cls, '_addedExtensions'): + cls._addedExtensions = True + cls.extensions = cls.extensions.copy() + for ext in rasterio.drivers.raster_driver_extensions(): + if ext not in cls.extensions: + cls.extensions[ext] = SourcePriority.IMPLICIT
+
+ + + +
+[docs] +def open(*args, **kwargs): + """Create an instance of the module class.""" + return RasterioFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """Check if an input can be read by the module class.""" + return RasterioFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_rasterio/girder_source.html b/_modules/large_image_source_rasterio/girder_source.html new file mode 100644 index 000000000..de84c615e --- /dev/null +++ b/_modules/large_image_source_rasterio/girder_source.html @@ -0,0 +1,164 @@ + + + + + + large_image_source_rasterio.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_rasterio.girder_source

+#############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+#############################################################################
+
+import packaging.version  # noqa F401
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import RasterioFileTileSource
+
+
+
+[docs] +class RasterioGirderTileSource(RasterioFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items for rasterio layers. + """ + + name = 'rasterio' + cacheName = 'tilesource' + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs): + projection = kwargs.get('projection', args[1] if len(args) >= 2 else None) + unitPerPixel = kwargs.get('unitsPerPixel', args[3] if len(args) >= 4 else None) + + return ( + GirderTileSource.getLRUHash(*args, **kwargs) + + f',{projection},{unitPerPixel}' + )
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_test.html b/_modules/large_image_source_test.html new file mode 100644 index 000000000..333ec6035 --- /dev/null +++ b/_modules/large_image_source_test.html @@ -0,0 +1,497 @@ + + + + + + large_image_source_test — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_test

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import colorsys
+import itertools
+import math
+import re
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import numpy as np
+from PIL import Image, ImageDraw, ImageFont
+
+from large_image.cache_util import LruCacheMetaclass, methodcache, strhash
+from large_image.constants import TILE_FORMAT_NUMPY, TILE_FORMAT_PIL, SourcePriority
+from large_image.exceptions import TileSourceError
+from large_image.tilesource import TileSource
+from large_image.tilesource.utilities import _imageToNumpy, _imageToPIL
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+_counters = {
+    'tiles': 0,
+}
+
+
+
+[docs] +class TestTileSource(TileSource, metaclass=LruCacheMetaclass): + cacheName = 'tilesource' + name = 'test' + extensions = { + None: SourcePriority.MANUAL, + } + + def __init__(self, ignored_path=None, minLevel=0, maxLevel=9, + tileWidth=256, tileHeight=256, sizeX=None, sizeY=None, + fractal=False, frames=None, monochrome=False, bands=None, + **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param ignored_path: for compatibility with FileTileSource. + :param minLevel: minimum tile level + :param maxLevel: maximum tile level. If both sizeX and sizeY are + specified, this value is ignored. + :param tileWidth: tile width in pixels + :param tileHeight: tile height in pixels + :param sizeX: image width in pixels at maximum level. Computed from + maxLevel and tileWidth if None. + :param sizeY: image height in pixels at maximum level. Computed from + maxLevel and tileHeight if None. + :param fractal: if True, and the tile size is square and a power of + two, draw a simple fractal on the tiles. + :param frames: if present, this is either a single number for generic + frames, a comma-separated list of c,z,t,xy, or a string of the + form '<axis>=<count>,<axis>=<count>,...'. + :param monochrome: if True, return single channel tiles. + :param bands: if present, a comma-separated list of band names. + Defaults to red,green,blue. Each band may optionally specify a + value range in the form "<band name>=<min val>-<max val>". If any + ranges are specified, bands with no ranges will use the union of + the specified ranges. The internal dtype with be uint8, uint16, or + float depending on the union of the specified ranges. If no ranges + are specified at all, it is the same as 0-255. + """ + if not kwargs.get('encoding'): + kwargs = kwargs.copy() + kwargs['encoding'] = 'PNG' + super().__init__(**kwargs) + + self._spec = ( + minLevel, maxLevel, tileWidth, tileHeight, sizeX, sizeY, fractal, + frames, monochrome, bands) + self.minLevel = minLevel + self.maxLevel = maxLevel + self.tileWidth = tileWidth + self.tileHeight = tileHeight + # Don't generate a fractal tile if the tile isn't square or not a power + # of 2 in size. + self.fractal = (fractal and self.tileWidth == self.tileHeight and + not (self.tileWidth & (self.tileWidth - 1))) + self.sizeX = (((2 ** self.maxLevel) * self.tileWidth) + if not sizeX else sizeX) + self.sizeY = (((2 ** self.maxLevel) * self.tileHeight) + if not sizeY else sizeY) + self.maxLevel = max(0, int(math.ceil(math.log2(max( + self.sizeX / self.tileWidth, self.sizeY / self.tileHeight))))) + self.minLevel = min(self.minLevel, self.maxLevel) + self.monochrome = bool(monochrome) + self._bands = None + self._dtype = np.uint8 + if bands: + bands = [re.match( + r'^(?P<key>[^=]+)(|=(?P<low>[+-]?((\d+(|\.\d*)))|(\.\d+))-(?P<high>[+-]?((\d+(|\.\d*))|(\.\d+))))$', # noqa + band) for band in bands.split(',')] + lows = [float(band.group('low')) + if band.group('low') is not None else None for band in bands] + highs = [float(band.group('high')) + if band.group('high') is not None else None for band in bands] + try: + low = min(v for v in lows + highs if v is not None) + high = max(v for v in lows + highs if v is not None) + except ValueError: + low = 0 + high = 255 + self._bands = { + band.group('key'): { + 'low': lows[idx] if lows[idx] is not None else low, + 'high': highs[idx] if highs[idx] is not None else high, + } + for idx, band in enumerate(bands)} + if low < 0 or high < 2 or low >= 65536 or high >= 65536: + self._dtype = float + elif low >= 256 or high >= 256: + self._dtype = np.uint16 + # Used for reporting tile information + self.levels = self.maxLevel + 1 + if frames: + frameList = [] + if '=' not in str(frames) and ',' not in str(frames): + self._axes = [('f', 'Index', int(frames))] + elif '=' not in str(frames): + self._axes = [ + (axis, f'Index{axis.upper()}', int(part)) + for axis, part in zip(['c', 'z', 't', 'xy'], frames.split(','))] + else: + self._axes = [ + (part.split('=', 1)[0], + f'Index{part.split("=", 1)[0].upper()}', + int(part.split('=', 1)[1])) for part in frames.split(',')] + self._framesParts = len(self._axes) + axes = self._axes[::-1] + for fidx in itertools.product(*(range(part[-1]) for part in axes)): + curframe = {} + for idx in range(len(fidx)): + k = axes[idx][1] + v = fidx[idx] + if axes[idx][-1] > 1: + curframe[k] = v + frameList.append(curframe) + if len(frameList) > 1: + self._frames = frameList + +
+[docs] + @classmethod + def canRead(cls, *args, **kwargs): + return True
+ + +
+[docs] + def fractalTile(self, image, x, y, widthCount, color=(0, 0, 0)): + """ + Draw a simple fractal in a tile image. + + :param image: a Pil image to draw on. Modified. + :param x: the tile x position + :param y: the tile y position + :param widthCount: 2 ** z; the number of tiles across for a "full size" + image at this z level. + :param color: an rgb tuple on a scale of [0-255]. + """ + imageDraw = ImageDraw.Draw(image) + x *= self.tileWidth + y *= self.tileHeight + sq = widthCount * self.tileWidth + while sq >= 4: + sq1 = sq // 4 + sq2 = sq1 + sq // 2 + for t in range(-(y % sq), self.tileWidth, sq): + if t + sq1 < self.tileWidth and t + sq2 >= 0: + for l in range(-(x % sq), self.tileWidth, sq): + if l + sq1 < self.tileWidth and l + sq2 >= 0: + imageDraw.rectangle([ + max(-1, l + sq1), max(-1, t + sq1), + min(self.tileWidth, l + sq2 - 1), + min(self.tileWidth, t + sq2 - 1), + ], color, None) + sq //= 2
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + result = super().getMetadata() + if hasattr(self, '_frames') and len(self._frames) > 1: + result['frames'] = self._frames + self._addMetadataFrameInformation(result) + if self._bands: + result['bands'] = {n + 1: {'interpretation': val} + for n, val in enumerate(self._bands)} + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + return {'fractal': self.fractal, 'monochrome': self.monochrome}
+ + + def _tileImage(self, rgbColor, x, y, z, frame, band=None, bandnum=0): + image = Image.new( + mode='RGB', + size=(self.tileWidth, self.tileHeight), + color=(rgbColor if not self.fractal else (255, 255, 255)), + ) + if self.fractal: + self.fractalTile(image, x, y, 2 ** z, rgbColor) + + bandtext = '\n' if band is not None else '' + if bandnum and band and band.lower() not in { + 'r', 'red', 'g', 'green', 'b', 'blue', 'grey', 'gray', 'alpha'}: + bandtext += band + image = _imageToNumpy(image)[0].astype(float) + vstripe = np.array([ + int(x / (self.tileWidth / bandnum / 2)) % 2 + for x in range(self.tileWidth)]) + hstripe = np.array([ + int(y / (self.tileHeight / (bandnum % self.tileWidth) / 2)) % 2 + if bandnum > self.tileWidth else 1 for y in range(self.tileHeight)]) + simage = image.copy() + simage[hstripe == 0, :, :] /= 2 + simage[:, vstripe == 0, :] /= 2 + image = np.where(image != 255, simage, image) + image = image.astype(np.uint8) + image = _imageToPIL(image) + + imageDraw = ImageDraw.Draw(image) + + fontsize = 0.15 + text = 'x=%d\ny=%d\nz=%d' % (x, y, z) + if hasattr(self, '_frames'): + for k1, k2, _ in self._axes: + if k2 in self._frames[frame]: + text += '\n%s=%d' % (k1.upper(), self._frames[frame][k2]) + text += bandtext + fontsize = min(fontsize, 0.8 / len(text.split('\n'))) + try: + # the font size should fill the whole tile + imageDrawFont = ImageFont.truetype( + font='/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf', + size=int(fontsize * min(self.tileWidth, self.tileHeight)), + ) + except OSError: + imageDrawFont = ImageFont.load_default() + imageDraw.multiline_text( + xy=(10, 10), + text=text, + fill=(0, 0, 0) if band != 'alpha' else (255, 255, 255), + font=imageDrawFont, + ) + return image + +
+[docs] + @methodcache() + def getTile(self, x, y, z, *args, **kwargs): + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame, len(self._frames) if hasattr(self, '_frames') else None) + + if not (self.minLevel <= z <= self.maxLevel): + msg = 'z layer does not exist' + raise TileSourceError(msg) + _counters['tiles'] += 1 + + xFraction = (x + 0.5) * self.tileWidth * 2 ** (self.levels - 1 - z) / self.sizeX + yFraction = (y + 0.5) * self.tileHeight * 2 ** (self.levels - 1 - z) / self.sizeY + fFraction = yFraction + if hasattr(self, '_frames'): + fFraction = float(frame) / (len(self._frames) - 1) + + backgroundColor = colorsys.hsv_to_rgb( + h=xFraction, + s=(0.3 + (0.7 * fFraction)), + v=(0.3 + (0.7 * yFraction)), + ) + rgbColor = tuple(int(val * 255) for val in backgroundColor) + + if not self._bands or len(self._bands) == (1 if self.monochrome else 3): + image = self._tileImage(rgbColor, x, y, z, frame) + if self.monochrome: + image = image.convert('L') + format = TILE_FORMAT_PIL + else: + image = np.zeros( + (self.tileHeight, self.tileWidth, len(self._bands)), dtype=self._dtype) + for bandnum, band in enumerate(self._bands): + bandimg = self._tileImage(rgbColor, x, y, z, frame, band, bandnum) + if self.monochrome or band.upper() in {'grey', 'gray', 'alpha'}: + bandimg = bandimg.convert('L') + bandimg = _imageToNumpy(bandimg)[0] + if (self._dtype != np.uint8 or + self._bands[band]['low'] != 0 or + self._bands[band]['high'] != 255): + bandimg = bandimg.astype(float) + bandimg = (bandimg / 255) * ( + self._bands[band]['high'] - self._bands[band]['low'] + ) + self._bands[band]['low'] + bandimg = bandimg.astype(self._dtype) + image[:, :, bandnum] = bandimg[:, :, bandnum % bandimg.shape[2]] + format = TILE_FORMAT_NUMPY + return self._outputTile(image, format, x, y, z, **kwargs)
+ + +
+[docs] + @staticmethod + def getLRUHash(*args, **kwargs): + return strhash( + super(TestTileSource, TestTileSource).getLRUHash( + *args, **kwargs), + kwargs.get('minLevel'), kwargs.get('maxLevel'), + kwargs.get('tileWidth'), kwargs.get('tileHeight'), + kwargs.get('fractal'), kwargs.get('sizeX'), kwargs.get('sizeY'), + kwargs.get('frames'), kwargs.get('monochrome'), + kwargs.get('bands'), + )
+ + +
+[docs] + def getState(self): + return 'test %r %r' % (super().getState(), self._spec)
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return TestTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return TestTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_tiff.html b/_modules/large_image_source_tiff.html new file mode 100644 index 000000000..496f3a478 --- /dev/null +++ b/_modules/large_image_source_tiff.html @@ -0,0 +1,947 @@ + + + + + + large_image_source_tiff — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_tiff

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+import base64
+import io
+import itertools
+import json
+import math
+import os
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import cachetools
+import numpy as np
+import PIL.Image
+import tifftools
+
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_NUMPY, TILE_FORMAT_PIL, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource, nearPowerOfTwo
+
+from . import tiff_reader
+from .exceptions import (InvalidOperationTiffError, IOOpenTiffError,
+                         IOTiffError, TiffError, ValidationTiffError)
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+@cachetools.cached(cache=cachetools.LRUCache(maxsize=10))
+def _cached_read_tiff(path):
+    return tifftools.read_tiff(path)
+
+
+
+[docs] +class TiffFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to TIFF files. + """ + + cacheName = 'tilesource' + name = 'tiff' + extensions = { + None: SourcePriority.MEDIUM, + 'tif': SourcePriority.HIGH, + 'tiff': SourcePriority.HIGH, + 'ptif': SourcePriority.PREFERRED, + 'ptiff': SourcePriority.PREFERRED, + 'qptiff': SourcePriority.PREFERRED, + 'svs': SourcePriority.MEDIUM, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'image/tiff': SourcePriority.HIGH, + 'image/x-tiff': SourcePriority.HIGH, + 'image/x-ptif': SourcePriority.PREFERRED, + } + + _maxAssociatedImageSize = 8192 + _maxUntiledImage = 4096 + + def __init__(self, path, **kwargs): # noqa + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + self._largeImagePath = str(self._getLargeImagePath()) + + lastException = None + try: + self._initWithTiffTools() + return + except Exception as exc: + self.logger.debug('Cannot read with tifftools route; %r', exc) + lastException = exc + + alldir = [] + try: + if hasattr(self, '_info'): + alldir = self._scanDirectories() + except IOOpenTiffError: + msg = 'File cannot be opened via tiff source.' + raise TileSourceError(msg) + except (ValidationTiffError, TiffError) as exc: + lastException = exc + + # If there are no tiled images, raise an exception. + if not len(alldir): + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + msg = "File %s didn't meet requirements for tile source: %s" % ( + self._largeImagePath, lastException) + self.logger.debug(msg) + raise TileSourceError(msg) + # Sort the known directories by image area (width * height). Given + # equal area, sort by the level. + alldir.sort() + # The highest resolution image is our preferred image + highest = alldir[-1][-1] + directories = {} + # Discard any images that use a different tiling scheme than our + # preferred image + for tdir in alldir: + td = tdir[-1] + level = tdir[2] + if (td.tileWidth != highest.tileWidth or + td.tileHeight != highest.tileHeight): + if not len(self._associatedImages): + self._addAssociatedImage(tdir[-2], True, highest) + continue + # If a layer's image is not a multiple of the tile size, it should + # be near a power of two of the highest resolution image. + if (((td.imageWidth % td.tileWidth) and + not nearPowerOfTwo(td.imageWidth, highest.imageWidth)) or + ((td.imageHeight % td.tileHeight) and + not nearPowerOfTwo(td.imageHeight, highest.imageHeight))): + continue + # If a layer is a multiple of the tile size, the number of tiles + # should be a power of two rounded up from the primary. + if (not (td.imageWidth % td.tileWidth) and not (td.imageHeight % td.tileHeight)): + htw = highest.imageWidth // td.tileWidth + hth = highest.imageHeight // td.tileHeight + ttw = td.imageWidth // td.tileWidth + tth = td.imageHeight // td.tileHeight + while (htw > ttw and htw > 1) or (hth > tth and hth > 1): + htw = (htw + 1) // 2 + hth = (hth + 1) // 2 + if htw != ttw or hth != tth: + continue + directories[level] = td + if not len(directories) or (len(directories) < 2 and max(directories.keys()) + 1 > 4): + msg = 'Tiff image must have at least two levels.' + raise TileSourceError(msg) + + sampleformat = highest._tiffInfo.get('sampleformat') + bitspersample = highest._tiffInfo.get('bitspersample') + self._dtype = np.dtype('%s%d' % ( + tifftools.constants.SampleFormat[sampleformat or 1].name, + bitspersample, + )) + self._bandCount = highest._tiffInfo.get('samplesperpixel', 1) + # Sort the directories so that the highest resolution is the last one; + # if a level is missing, put a None value in its place. + self._tiffDirectories = [directories.get(key) for key in + range(max(directories.keys()) + 1)] + self.tileWidth = highest.tileWidth + self.tileHeight = highest.tileHeight + self.levels = len(self._tiffDirectories) + self.sizeX = highest.imageWidth + self.sizeY = highest.imageHeight + self._checkForInefficientDirectories() + self._checkForVendorSpecificTags() + +
+[docs] + def getTiffDir(self, directoryNum, mustBeTiled=True, subDirectoryNum=0, validate=True): + """ + Get a tile tiff directory reader class. + + :param directoryNum: The number of the TIFF image file directory to + open. + :param mustBeTiled: if True, only tiled images validate. If False, + only non-tiled images validate. None validates both. + :param subDirectoryNum: if set, the number of the TIFF subdirectory. + :param validate: if False, don't validate that images can be read. + :returns: a class that can read from a specific tiff directory. + """ + return tiff_reader.TiledTiffDirectory( + filePath=self._largeImagePath, + directoryNum=directoryNum, + mustBeTiled=mustBeTiled, + subDirectoryNum=subDirectoryNum, + validate=validate)
+ + + def _scanDirectories(self): + lastException = None + # Associated images are smallish TIFF images that have an image + # description and are not tiled. They have their own TIFF directory. + # Individual TIFF images can also have images embedded into their + # directory as tags (this is a vendor-specific method of adding more + # images into a file) -- those are stored in the individual + # directories' _embeddedImages field. + self._associatedImages = {} + + dir = None + # Query all know directories in the tif file. Only keep track of + # directories that contain tiled images. + alldir = [] + associatedDirs = [] + for directoryNum in itertools.count(): # pragma: no branch + try: + if dir is None: + dir = self.getTiffDir(directoryNum, validate=False) + else: + dir._setDirectory(directoryNum) + dir._loadMetadata() + dir._validate() + except ValidationTiffError as exc: + lastException = exc + associatedDirs.append(directoryNum) + continue + except TiffError as exc: + if not lastException: + lastException = exc + break + if not dir.tileWidth or not dir.tileHeight: + continue + # Calculate the tile level, where 0 is a single tile, 1 is up to a + # set of 2x2 tiles, 2 is 4x4, etc. + level = int(math.ceil(math.log(max( + float(dir.imageWidth) / dir.tileWidth, + float(dir.imageHeight) / dir.tileHeight)) / math.log(2))) + if level < 0: + continue + td, dir = dir, None + # Store information for sorting with the directory. + alldir.append((level > 0, td.tileWidth * td.tileHeight, level, + td.imageWidth * td.imageHeight, directoryNum, td)) + if not alldir and lastException: + raise lastException + for directoryNum in associatedDirs: + self._addAssociatedImage(directoryNum) + return alldir + + def _levelFromIfd(self, ifd, baseifd): + """ + Get the level based on information in an ifd and on the full-resolution + 0-frame ifd. An exception is raised if the ifd does not seem to + represent a possible level. + + :param ifd: an ifd record returned from tifftools. + :param baseifd: the ifd record of the full-resolution frame 0. + :returns: the level, where self.levels - 1 is full resolution and 0 is + the lowest resolution. + """ + sizeX = ifd['tags'][tifftools.Tag.ImageWidth.value]['data'][0] + sizeY = ifd['tags'][tifftools.Tag.ImageLength.value]['data'][0] + if tifftools.Tag.TileWidth.value in baseifd['tags']: + tileWidth = baseifd['tags'][tifftools.Tag.TileWidth.value]['data'][0] + tileHeight = baseifd['tags'][tifftools.Tag.TileLength.value]['data'][0] + else: + tileWidth = sizeX + tileHeight = baseifd['tags'][tifftools.Tag.RowsPerStrip.value]['data'][0] + + for tag in { + tifftools.Tag.SamplesPerPixel.value, + tifftools.Tag.BitsPerSample.value, + tifftools.Tag.PlanarConfig.value, + tifftools.Tag.Photometric.value, + tifftools.Tag.Orientation.value, + tifftools.Tag.Compression.value, + tifftools.Tag.TileWidth.value, + tifftools.Tag.TileLength.value, + }: + if ((tag in ifd['tags'] and tag not in baseifd['tags']) or + (tag not in ifd['tags'] and tag in baseifd['tags']) or + (tag in ifd['tags'] and + ifd['tags'][tag]['data'] != baseifd['tags'][tag]['data'])): + msg = 'IFD does not match first IFD.' + raise TileSourceError(msg) + sizes = [(self.sizeX, self.sizeY)] + for level in range(self.levels - 1, -1, -1): + if (sizeX, sizeY) in sizes: + return level + altsizes = [] + for w, h in sizes: + w2f = int(math.floor(w / 2)) + h2f = int(math.floor(h / 2)) + w2c = int(math.ceil(w / 2)) + h2c = int(math.ceil(h / 2)) + w2t = int(math.floor((w / 2 + tileWidth - 1) / tileWidth)) * tileWidth + h2t = int(math.floor((h / 2 + tileHeight - 1) / tileHeight)) * tileHeight + for w2, h2 in [(w2f, h2f), (w2f, h2c), (w2c, h2f), (w2c, h2c), (w2t, h2t)]: + if (w2, h2) not in altsizes: + altsizes.append((w2, h2)) + sizes = altsizes + msg = 'IFD size is not a power of two smaller than first IFD.' + raise TileSourceError(msg) + + def _initWithTiffTools(self): # noqa + """ + Use tifftools to read all of the tiff directory information. Check if + the zeroth directory can be validated as a tiled directory. If so, + then check if the remaining directories are either tiled in descending + size or have subifds with tiles in descending sizes. All primary tiled + directories are the same size and format; all non-tiled directories are + treated as associated images. + """ + dir0 = self.getTiffDir(0, mustBeTiled=None) + self.tileWidth = dir0.tileWidth + self.tileHeight = dir0.tileHeight + self.sizeX = dir0.imageWidth + self.sizeY = dir0.imageHeight + self.levels = max(1, int(math.ceil(math.log(max( + dir0.imageWidth / dir0.tileWidth, + dir0.imageHeight / dir0.tileHeight)) / math.log(2))) + 1) + sampleformat = dir0._tiffInfo.get('sampleformat') + bitspersample = dir0._tiffInfo.get('bitspersample') + self._dtype = np.dtype('%s%d' % ( + tifftools.constants.SampleFormat[sampleformat or 1].name, + bitspersample, + )) + self._bandCount = dir0._tiffInfo.get('samplesperpixel', 1) + info = _cached_read_tiff(self._largeImagePath) + self._info = info + frames = [] + associated = [] # for now, a list of directories + for idx, ifd in enumerate(info['ifds']): + # if not tiles, add to associated images + if tifftools.Tag.tileWidth.value not in ifd['tags']: + associated.append(idx) + continue + level = self._levelFromIfd(ifd, info['ifds'][0]) + # if the same resolution as the main image, add a frame + if level == self.levels - 1: + frames.append({'dirs': [None] * self.levels}) + frames[-1]['dirs'][-1] = (idx, 0) + try: + frameMetadata = json.loads( + ifd['tags'][tifftools.Tag.ImageDescription.value]['data']) + for key in {'channels', 'frame'}: + if key in frameMetadata: + frames[-1][key] = frameMetadata[key] + except Exception: + pass + if tifftools.Tag.ICCProfile.value in ifd['tags']: + if not hasattr(self, '_iccprofiles'): + self._iccprofiles = [] + while len(self._iccprofiles) < len(frames) - 1: + self._iccprofiles.append(None) + self._iccprofiles.append(ifd['tags'][ + tifftools.Tag.ICCProfile.value]['data']) + # otherwise, add to the first frame missing that level + elif level < self.levels - 1 and any( + frame for frame in frames if frame['dirs'][level] is None): + frames[next( + idx for idx, frame in enumerate(frames) if frame['dirs'][level] is None + )]['dirs'][level] = (idx, 0) + else: + msg = 'Tile layers are in a surprising order' + raise TileSourceError(msg) + # if there are sub ifds, add them + if tifftools.Tag.SubIfd.value in ifd['tags']: + for subidx, subifds in enumerate(ifd['tags'][tifftools.Tag.SubIfd.value]['ifds']): + if len(subifds) != 1: + msg = 'When stored in subifds, each subifd should be a single ifd.' + raise TileSourceError(msg) + level = self._levelFromIfd(subifds[0], info['ifds'][0]) + if level < self.levels - 1 and frames[-1]['dirs'][level] is None: + frames[-1]['dirs'][level] = (idx, subidx + 1) + else: + msg = 'Tile layers are in a surprising order' + raise TileSourceError(msg) + # If we have a single untiled ifd that is "small", use it + if tifftools.Tag.tileWidth.value not in info['ifds'][0]['tags']: + if ( + self.sizeX > self._maxUntiledImage or self.sizeY > self._maxUntiledImage or + (len(info['ifds']) != 1 or tifftools.Tag.SubIfd.value in ifd['tags']) or + (tifftools.Tag.ImageDescription.value in ifd['tags'] and + 'ImageJ' in ifd['tags'][tifftools.Tag.ImageDescription.value]['data']) + ): + msg = 'A tiled TIFF is required.' + raise ValidationTiffError(msg) + associated = [] + level = self._levelFromIfd(ifd, info['ifds'][0]) + frames.append({'dirs': [None] * self.levels}) + frames[-1]['dirs'][-1] = (idx, 0) + try: + frameMetadata = json.loads( + ifd['tags'][tifftools.Tag.ImageDescription.value]['data']) + for key in {'channels', 'frame'}: + if key in frameMetadata: + frames[-1][key] = frameMetadata[key] + except Exception: + pass + if tifftools.Tag.ICCProfile.value in ifd['tags']: + if not hasattr(self, '_iccprofiles'): + self._iccprofiles = [] + while len(self._iccprofiles) < len(frames) - 1: + self._iccprofiles.append(None) + self._iccprofiles.append(ifd['tags'][ + tifftools.Tag.ICCProfile.value]['data']) + self._associatedImages = {} + for dirNum in associated: + self._addAssociatedImage(dirNum) + self._frames = frames + self._tiffDirectories = [ + self.getTiffDir( + frames[0]['dirs'][idx][0], + subDirectoryNum=frames[0]['dirs'][idx][1]) + if frames[0]['dirs'][idx] is not None else None + for idx in range(self.levels - 1)] + self._tiffDirectories.append(dir0) + self._checkForInefficientDirectories() + self._checkForVendorSpecificTags() + return True + + def _checkForInefficientDirectories(self, warn=True): + """ + Raise a warning for inefficient files. + + :param warn: if True and inefficient, emit a warning. + """ + self._populatedLevels = len([v for v in self._tiffDirectories if v is not None]) + missing = [v is None for v in self._tiffDirectories] + maxMissing = max(0 if not v else missing.index(False, idx) - idx + for idx, v in enumerate(missing)) + self._skippedLevels = maxMissing + if maxMissing >= self._maxSkippedLevels: + if warn: + self.logger.warning( + 'Tiff image is missing many lower resolution levels (%d). ' + 'It will be inefficient to read lower resolution tiles.', maxMissing) + self._inefficientWarning = True + + def _reorient_numpy_image(self, image, orientation): + """ + Reorient a numpy image array based on a tiff orientation. + + :param image: the numpy array to reorient. + :param orientation: one of the tiff orientation constants. + :returns: an image with top-left orientation. + """ + if len(image.shape) == 2: + image = np.resize(image, (image.shape[0], image.shape[1], 1)) + if orientation in { + tifftools.constants.Orientation.LeftTop.value, + tifftools.constants.Orientation.RightTop.value, + tifftools.constants.Orientation.LeftBottom.value, + tifftools.constants.Orientation.RightBottom.value}: + image = image.transpose(1, 0, 2) + if orientation in { + tifftools.constants.Orientation.BottomLeft.value, + tifftools.constants.Orientation.BottomRight.value, + tifftools.constants.Orientation.LeftBottom.value, + tifftools.constants.Orientation.RightBottom.value}: + image = image[::-1, ::, ::] + if orientation in { + tifftools.constants.Orientation.TopRight.value, + tifftools.constants.Orientation.BottomRight.value, + tifftools.constants.Orientation.RightTop.value, + tifftools.constants.Orientation.RightBottom.value}: + image = image[::, ::-1, ::] + return image + + def _checkForVendorSpecificTags(self): + if not hasattr(self, '_frames') or len(self._frames) <= 1: + return + if self._frames[0].get('frame', {}).get('IndexC'): + return + dir = self._tiffDirectories[-1] + if not hasattr(dir, '_description_record'): + return + if dir._description_record.get('PerkinElmer-QPI-ImageDescription', {}).get('Biomarker'): + channels = [] + for frame in range(len(self._frames)): + dir = self._getDirFromCache(*self._frames[frame]['dirs'][-1]) + channels.append(dir._description_record.get( + 'PerkinElmer-QPI-ImageDescription', {}).get('Biomarker')) + if channels[-1] is None: + return + self._frames[0]['channels'] = channels + for idx, frame in enumerate(self._frames): + frame.setdefault('frame', {}) + frame['frame']['IndexC'] = idx + + def _addAssociatedImage(self, directoryNum, mustBeTiled=False, topImage=None): + """ + Check if the specified TIFF directory contains an image with a sensible + image description that can be used as an ID. If so, and if the image + isn't too large, add this image as an associated image. + + :param directoryNum: libtiff directory number of the image. + :param mustBeTiled: if true, use tiled images. If false, require + untiled images. + :param topImage: if specified, add image-embedded metadata to this + image. + """ + try: + associated = self.getTiffDir(directoryNum, mustBeTiled) + id = '' + desc = associated._tiffInfo.get('imagedescription') + if desc: + id = desc.strip().split(None, 1)[0].lower() + if b'\n' in desc: + id = desc.split(b'\n', 1)[1].strip().split(None, 1)[0].lower() or id + elif mustBeTiled: + id = 'dir%d' % directoryNum + if not len(self._associatedImages): + id = 'macro' + if not id and not mustBeTiled: + id = {1: 'label', 9: 'macro'}.get(associated._tiffInfo.get('subfiletype')) + if not isinstance(id, str): + id = id.decode() + # Only use this as an associated image if the parsed id is + # a reasonable length, alphanumeric characters, and the + # image isn't too large. + if (id.isalnum() and len(id) > 3 and len(id) <= 20 and + associated._pixelInfo['width'] <= self._maxAssociatedImageSize and + associated._pixelInfo['height'] <= self._maxAssociatedImageSize and + id not in self._associatedImages): + image = associated._tiffFile.read_image() + # Optrascan scanners store xml image descriptions in a "tiled + # image". Check if this is the case, and, if so, parse such + # data + if image.tobytes()[:6] == b'<?xml ': + self._parseImageXml(image.tobytes().rsplit(b'>', 1)[0] + b'>', topImage) + return + image = self._reorient_numpy_image(image, associated._tiffInfo.get('orientation')) + self._associatedImages[id] = image + except (TiffError, AttributeError): + # If we can't validate or read an associated image or it has no + # useful imagedescription, fail quietly without adding an + # associated image. + pass + except Exception: + # If we fail for other reasons, don't raise an exception, but log + # what happened. + self.logger.exception( + 'Could not use non-tiled TIFF image as an associated image.') + + def _parseImageXml(self, xml, topImage): + """ + Parse metadata stored in arbitrary xml and associate it with a specific + image. + + :param xml: the xml as a string or bytes object. + :param topImage: the image to add metadata to. + """ + if not topImage or topImage.pixelInfo.get('magnificaiton'): + return + topImage.parse_image_description(xml) + if not topImage._description_record: + return + try: + xml = topImage._description_record + # Optrascan metadata + scanDetails = xml.get('ScanInfo', xml.get('EncodeInfo'))['ScanDetails'] + mag = float(scanDetails['Magnification']) + # In microns; convert to mm + scale = float(scanDetails['PixelResolution']) * 1e-3 + topImage._pixelInfo = { + 'magnification': mag, + 'mm_x': scale, + 'mm_y': scale, + } + except Exception: + pass + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + pixelInfo = self._tiffDirectories[-1].pixelInfo + mm_x = pixelInfo.get('mm_x') + mm_y = pixelInfo.get('mm_y') + # Estimate the magnification if we don't have a direct value + mag = pixelInfo.get('magnification') or 0.01 / mm_x if mm_x else None + return { + 'magnification': mag, + 'mm_x': mm_x, + 'mm_y': mm_y, + }
+ + + def _xmlToMetadata(self, xml): + if not isinstance(xml, dict) or set(xml.keys()) != {'DataObject'}: + return xml + values = {} + try: + objlist = xml['DataObject'] + if not isinstance(objlist, list): + objlist = [objlist] + for obj in objlist: + attrList = obj['Attribute'] + if not isinstance(attrList, list): + attrList = [attrList] + for attr in attrList: + if 'Array' not in attr: + values[attr['Name']] = attr.get('text', '') + else: + if 'DataObject' in attr['Array']: + subvalues = self._xmlToMetadata(attr['Array']) + for key, subvalue in subvalues.items(): + if key not in {'PIM_DP_IMAGE_DATA'}: + values[attr['Name'] + '|' + key] = subvalue + except Exception: + return xml + return values + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + result = super().getMetadata() + if hasattr(self, '_frames') and len(self._frames) > 1: + result['frames'] = [frame.get('frame', {}) for frame in self._frames] + self._addMetadataFrameInformation(result, self._frames[0].get('channels', None)) + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + results = {} + for idx, dir in enumerate(self._tiffDirectories[::-1]): + if dir: + if hasattr(dir, '_description_record'): + results['xml' + ( + '' if not results.get('xml') else '_' + str(idx))] = self._xmlToMetadata( + dir._description_record) + for k, v in dir._tiffInfo.items(): + if k == 'imagedescription' and hasattr(dir, '_description_record'): + continue + if isinstance(v, (str, bytes)) and k: + if isinstance(v, bytes): + try: + v = v.decode() + except UnicodeDecodeError: + continue + results.setdefault('tiff', {}) + if not idx and k not in results['tiff']: + results['tiff'][k] = v + elif k not in results['tiff'] or v != results['tiff'][k]: + results['tiff'][k + ':%d' % idx] = v + return results
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, + sparseFallback=False, **kwargs): + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame, len(self._frames) if hasattr(self, '_frames') else None) + if frame > 0: + if hasattr(self, '_frames') and self._frames[frame]['dirs'][z] is not None: + dir = self._getDirFromCache(*self._frames[frame]['dirs'][z]) + else: + dir = None + else: + dir = self._tiffDirectories[z] + try: + allowStyle = True + if dir is None: + try: + if not kwargs.get('inSparseFallback'): + tile = self._getTileFromEmptyLevel(x, y, z, **kwargs) + else: + raise IOTiffError('Missing z level %d' % z) + except Exception: + if sparseFallback: + raise IOTiffError('Missing z level %d' % z) + else: + raise + allowStyle = False + format = TILE_FORMAT_PIL + else: + tile = dir.getTile(x, y, asarray=numpyAllowed == 'always') + format = 'JPEG' + if isinstance(tile, PIL.Image.Image): + format = TILE_FORMAT_PIL + if isinstance(tile, np.ndarray): + format = TILE_FORMAT_NUMPY + return self._outputTile(tile, format, x, y, z, pilImageAllowed, + numpyAllowed, applyStyle=allowStyle, **kwargs) + except InvalidOperationTiffError as e: + raise TileSourceError(e.args[0]) + except IOTiffError as e: + return self.getTileIOTiffError( + x, y, z, pilImageAllowed=pilImageAllowed, + numpyAllowed=numpyAllowed, sparseFallback=sparseFallback, + exception=e, **kwargs)
+ + + def _getDirFromCache(self, dirnum, subdir=None): + if not hasattr(self, '_directoryCache') or not hasattr(self, '_directoryCacheMaxSize'): + self._directoryCache = {} + self._directoryCacheMaxSize = max(20, self.levels * (2 + ( + self.metadata.get('IndexRange', {}).get('IndexC', 1)))) + key = (dirnum, subdir) + result = self._directoryCache.get(key) + if result is None: + if len(self._directoryCache) >= self._directoryCacheMaxSize: + self._directoryCache = {} + try: + result = self.getTiffDir(dirnum, mustBeTiled=None, subDirectoryNum=subdir) + except IOTiffError: + result = None + self._directoryCache[key] = result + return result + +
+[docs] + def getTileIOTiffError(self, x, y, z, pilImageAllowed=False, + numpyAllowed=False, sparseFallback=False, + exception=None, **kwargs): + if sparseFallback: + if z: + noedge = kwargs.copy() + noedge.pop('edge', None) + noedge['inSparseFallback'] = True + image = self.getTile( + x // 2, y // 2, z - 1, pilImageAllowed=True, numpyAllowed=False, + sparseFallback=sparseFallback, edge=False, + **noedge) + if not isinstance(image, PIL.Image.Image): + image = PIL.Image.open(io.BytesIO(image)) + image = image.crop(( + self.tileWidth / 2 if x % 2 else 0, + self.tileHeight / 2 if y % 2 else 0, + self.tileWidth if x % 2 else self.tileWidth / 2, + self.tileHeight if y % 2 else self.tileHeight / 2)) + image = image.resize((self.tileWidth, self.tileHeight)) + else: + image = PIL.Image.new('RGBA', (self.tileWidth, self.tileHeight)) + return self._outputTile(image, TILE_FORMAT_PIL, x, y, z, pilImageAllowed, + numpyAllowed, applyStyle=False, **kwargs) + raise TileSourceError('Internal I/O failure: %s' % exception.args[0])
+ + + def _nonemptyLevelsList(self, frame=0): + """ + Return a list of one value per level where the value is None if the + level does not exist in the file and any other value if it does. + + :param frame: the frame number. + :returns: a list of levels length. + """ + dirlist = self._tiffDirectories + frame = int(frame or 0) + if frame > 0 and hasattr(self, '_frames'): + dirlist = self._frames[frame]['dirs'] + return dirlist + +
+[docs] + def getAssociatedImagesList(self): + """ + Get a list of all associated images. + + :return: the list of image keys. + """ + imageList = set(self._associatedImages) + for td in self._tiffDirectories: + if td is not None: + imageList |= set(td._embeddedImages) + return sorted(imageList)
+ + + def _getAssociatedImage(self, imageKey): + """ + Get an associated image in PIL format. + + :param imageKey: the key of the associated image. + :return: the image in PIL format or None. + """ + # The values in _embeddedImages are sometimes duplicated with the + # _associatedImages. There are some sample files where libtiff's + # read_image fails to read the _associatedImage properly because of + # separated jpeg information. For the samples we currently have, + # preferring the _embeddedImages is sufficient, but if find other files + # with seemingly bad associated images, we may need to read them with a + # more complex process than read_image. + for td in self._tiffDirectories: + if td is not None and imageKey in td._embeddedImages: + return PIL.Image.open(io.BytesIO(base64.b64decode(td._embeddedImages[imageKey]))) + if imageKey in self._associatedImages: + return PIL.Image.fromarray(self._associatedImages[imageKey])
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return TiffFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return TiffFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_tiff/exceptions.html b/_modules/large_image_source_tiff/exceptions.html new file mode 100644 index 000000000..045b23f1c --- /dev/null +++ b/_modules/large_image_source_tiff/exceptions.html @@ -0,0 +1,163 @@ + + + + + + large_image_source_tiff.exceptions — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_tiff.exceptions

+
+[docs] +class TiffError(Exception): + pass
+ + + +
+[docs] +class InvalidOperationTiffError(TiffError): + """ + An exception caused by the user making an invalid request of a TIFF file. + """
+ + + +
+[docs] +class IOTiffError(TiffError): + """ + An exception caused by an internal failure, due to an invalid file or other + error. + """
+ + + +
+[docs] +class IOOpenTiffError(IOTiffError): + """ + An exception caused by an internal failure where the file cannot be opened + by the main library. + """
+ + + +
+[docs] +class ValidationTiffError(TiffError): + """ + An exception caused by the TIFF reader not being able to support a given + file. + """
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_tiff/girder_source.html b/_modules/large_image_source_tiff/girder_source.html new file mode 100644 index 000000000..c99ce71d2 --- /dev/null +++ b/_modules/large_image_source_tiff/girder_source.html @@ -0,0 +1,150 @@ + + + + + + large_image_source_tiff.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_tiff.girder_source

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import TiffFileTileSource
+
+
+
+[docs] +class TiffGirderTileSource(TiffFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with a TIFF file. + """ + + cacheName = 'tilesource' + name = 'tiff'
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_tiff/tiff_reader.html b/_modules/large_image_source_tiff/tiff_reader.html new file mode 100644 index 000000000..afd81e317 --- /dev/null +++ b/_modules/large_image_source_tiff/tiff_reader.html @@ -0,0 +1,1022 @@ + + + + + + large_image_source_tiff.tiff_reader — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_tiff.tiff_reader

+###############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+###############################################################################
+
+import ctypes
+import io
+import json
+import math
+import os
+import threading
+from functools import partial
+from xml.etree import ElementTree
+
+import cachetools
+import numpy as np
+import PIL.Image
+
+from large_image import config
+from large_image.cache_util import methodcache, strhash
+from large_image.tilesource import etreeToDict
+
+from .exceptions import InvalidOperationTiffError, IOOpenTiffError, IOTiffError, ValidationTiffError
+
+try:
+    from libtiff import libtiff_ctypes
+except ValueError as exc:
+    # If the python libtiff module doesn't contain a pregenerated module for
+    # the appropriate version of libtiff, it tries to generate a module from
+    # the libtiff header file.  If it can't find this file (possibly because it
+    # is in a virtual environment), it raises a ValueError instead of an
+    # ImportError.  We convert this to an ImportError, so that we will print a
+    # more lucid error message and just fail to load this one tile source
+    # instead of failing to load the whole plugin.
+    config.getLogger().warning(
+        'Failed to import libtiff; try upgrading the python module (%s)' % exc)
+    raise ImportError(str(exc))
+
+# This suppress warnings about unknown tags
+libtiff_ctypes.suppress_warnings()
+# Suppress errors to stderr
+libtiff_ctypes.suppress_errors()
+
+
+_ctypesFormattbl = {
+    (8, libtiff_ctypes.SAMPLEFORMAT_UINT): np.uint8,
+    (8, libtiff_ctypes.SAMPLEFORMAT_INT): np.int8,
+    (16, libtiff_ctypes.SAMPLEFORMAT_UINT): np.uint16,
+    (16, libtiff_ctypes.SAMPLEFORMAT_INT): np.int16,
+    (16, libtiff_ctypes.SAMPLEFORMAT_IEEEFP): np.float16,
+    (32, libtiff_ctypes.SAMPLEFORMAT_UINT): np.uint32,
+    (32, libtiff_ctypes.SAMPLEFORMAT_INT): np.int32,
+    (32, libtiff_ctypes.SAMPLEFORMAT_IEEEFP): np.float32,
+    (64, libtiff_ctypes.SAMPLEFORMAT_UINT): np.uint64,
+    (64, libtiff_ctypes.SAMPLEFORMAT_INT): np.int64,
+    (64, libtiff_ctypes.SAMPLEFORMAT_IEEEFP): np.float64,
+}
+
+
+
+[docs] +def patchLibtiff(): + libtiff_ctypes.libtiff.TIFFFieldWithTag.restype = \ + ctypes.POINTER(libtiff_ctypes.TIFFFieldInfo) + libtiff_ctypes.libtiff.TIFFFieldWithTag.argtypes = \ + (libtiff_ctypes.TIFF, libtiff_ctypes.c_ttag_t) + + # BigTIFF 64-bit unsigned integer + libtiff_ctypes.TIFFDataType.TIFF_LONG8 = 16 + # BigTIFF 64-bit signed integer + libtiff_ctypes.TIFFDataType.TIFF_SLONG8 = 17 + # BigTIFF 64-bit unsigned integer (offset) + libtiff_ctypes.TIFFDataType.TIFF_IFD8 = 18
+ + + +patchLibtiff() + + +
+[docs] +class TiledTiffDirectory: + + CoreFunctions = [ + 'SetDirectory', 'SetSubDirectory', 'GetField', + 'LastDirectory', 'GetMode', 'IsTiled', 'IsByteSwapped', 'IsUpSampled', + 'IsMSB2LSB', 'NumberOfStrips', + ] + + def __init__(self, filePath, directoryNum, mustBeTiled=True, subDirectoryNum=0, validate=True): + """ + Create a new reader for a tiled image file directory in a TIFF file. + + :param filePath: A path to a TIFF file on disk. + :type filePath: str + :param directoryNum: The number of the TIFF image file directory to + open. + :type directoryNum: int + :param mustBeTiled: if True, only tiled images validate. If False, + only non-tiled images validate. None validates both. + :type mustBeTiled: bool + :param subDirectoryNum: if set, the number of the TIFF subdirectory. + :type subDirectoryNum: int + :param validate: if False, don't validate that images can be read. + :type mustBeTiled: bool + :raises: InvalidOperationTiffError or IOTiffError or + ValidationTiffError + """ + self.logger = config.getLogger() + # create local cache to store Jpeg tables and getTileByteCountsType + self.cache = cachetools.LRUCache(10) + self._mustBeTiled = mustBeTiled + + self._tiffFile = None + self._tileLock = threading.RLock() + + self._open(filePath, directoryNum, subDirectoryNum) + self._loadMetadata() + self.logger.debug( + 'TiffDirectory %d:%d Information %r', + directoryNum, subDirectoryNum or 0, self._tiffInfo) + try: + if validate: + self._validate() + except ValidationTiffError: + self._close() + raise + + def __del__(self): + self._close() + + def _open(self, filePath, directoryNum, subDirectoryNum=0): + """ + Open a TIFF file to a given file and IFD number. + + :param filePath: A path to a TIFF file on disk. + :type filePath: str + :param directoryNum: The number of the TIFF IFD to be used. + :type directoryNum: int + :param subDirectoryNum: The number of the TIFF sub-IFD to be used. + :type subDirectoryNum: int + :raises: InvalidOperationTiffError or IOTiffError + """ + self._close() + if not os.path.isfile(filePath): + raise InvalidOperationTiffError( + 'TIFF file does not exist: %s' % filePath) + try: + bytePath = filePath + if not isinstance(bytePath, bytes): + bytePath = filePath.encode() + self._tiffFile = libtiff_ctypes.TIFF.open(bytePath) + except TypeError: + raise IOOpenTiffError( + 'Could not open TIFF file: %s' % filePath) + # pylibtiff changed the case of some functions between version 0.4 and + # the version that supports libtiff 4.0.6. To support both, ensure + # that the cased functions exist. + for func in self.CoreFunctions: + if (not hasattr(self._tiffFile, func) and + hasattr(self._tiffFile, func.lower())): + setattr(self._tiffFile, func, getattr( + self._tiffFile, func.lower())) + self._setDirectory(directoryNum, subDirectoryNum) + + def _setDirectory(self, directoryNum, subDirectoryNum=0): + self._directoryNum = directoryNum + if self._tiffFile.SetDirectory(self._directoryNum) != 1: + self._tiffFile.close() + raise IOTiffError( + 'Could not set TIFF directory to %d' % directoryNum) + self._subDirectoryNum = subDirectoryNum + if self._subDirectoryNum: + subifds = self._tiffFile.GetField('subifd') + if (subifds is None or self._subDirectoryNum < 1 or + self._subDirectoryNum > len(subifds)): + raise IOTiffError( + 'Could not set TIFF subdirectory to %d' % subDirectoryNum) + subifd = subifds[self._subDirectoryNum - 1] + if self._tiffFile.SetSubDirectory(subifd) != 1: + self._tiffFile.close() + raise IOTiffError( + 'Could not set TIFF subdirectory to %d' % subDirectoryNum) + + def _close(self): + if self._tiffFile: + self._tiffFile.close() + self._tiffFile = None + + def _validate(self): # noqa + """ + Validate that this TIFF file and directory are suitable for reading. + + :raises: ValidationTiffError + """ + if not self._mustBeTiled: + if self._mustBeTiled is not None and self._tiffInfo.get('istiled'): + msg = 'Expected a non-tiled TIFF file' + raise ValidationTiffError(msg) + # For any non-supported file, we probably can add a conversion task in + # the create_image.py script, such as flatten or colourspace. These + # should only be done if necessary, which would require the conversion + # job to check output and perform subsequent processing as needed. + if (not self._tiffInfo.get('samplesperpixel', 1) or + self._tiffInfo.get('samplesperpixel', 1) < 1): + msg = 'Only RGB and greyscale TIFF files are supported' + raise ValidationTiffError(msg) + + if self._tiffInfo.get('bitspersample') not in (8, 16, 32, 64): + msg = 'Only 8 and 16 bits-per-sample TIFF files are supported' + raise ValidationTiffError(msg) + + if self._tiffInfo.get('sampleformat') not in { + None, # default is still SAMPLEFORMAT_UINT + libtiff_ctypes.SAMPLEFORMAT_UINT, + libtiff_ctypes.SAMPLEFORMAT_INT, + libtiff_ctypes.SAMPLEFORMAT_IEEEFP}: + msg = 'Only unsigned int sampled TIFF files are supported' + raise ValidationTiffError(msg) + + if (self._tiffInfo.get('planarconfig') != libtiff_ctypes.PLANARCONFIG_CONTIG and + self._tiffInfo.get('photometric') not in { + libtiff_ctypes.PHOTOMETRIC_MINISBLACK}): + msg = 'Only contiguous planar configuration TIFF files are supported' + raise ValidationTiffError(msg) + + if self._tiffInfo.get('photometric') not in { + libtiff_ctypes.PHOTOMETRIC_MINISBLACK, + libtiff_ctypes.PHOTOMETRIC_RGB, + libtiff_ctypes.PHOTOMETRIC_YCBCR}: + msg = ('Only greyscale (black is 0), RGB, and YCbCr photometric ' + 'interpretation TIFF files are supported') + raise ValidationTiffError(msg) + + if self._tiffInfo.get('orientation') not in { + libtiff_ctypes.ORIENTATION_TOPLEFT, + libtiff_ctypes.ORIENTATION_TOPRIGHT, + libtiff_ctypes.ORIENTATION_BOTRIGHT, + libtiff_ctypes.ORIENTATION_BOTLEFT, + libtiff_ctypes.ORIENTATION_LEFTTOP, + libtiff_ctypes.ORIENTATION_RIGHTTOP, + libtiff_ctypes.ORIENTATION_RIGHTBOT, + libtiff_ctypes.ORIENTATION_LEFTBOT, + None}: + msg = 'Unsupported TIFF orientation' + raise ValidationTiffError(msg) + + if self._mustBeTiled and ( + not self._tiffInfo.get('istiled') or + not self._tiffInfo.get('tilewidth') or + not self._tiffInfo.get('tilelength')): + msg = 'A tiled TIFF is required.' + raise ValidationTiffError(msg) + + if self._mustBeTiled is False and ( + self._tiffInfo.get('istiled') or + not self._tiffInfo.get('rowsperstrip')): + msg = 'A non-tiled TIFF with strips is required.' + raise ValidationTiffError(msg) + + if (self._tiffInfo.get('compression') == libtiff_ctypes.COMPRESSION_JPEG and + self._tiffInfo.get('jpegtablesmode') != + libtiff_ctypes.JPEGTABLESMODE_QUANT | + libtiff_ctypes.JPEGTABLESMODE_HUFF): + msg = 'Only TIFF files with separate Huffman and quantization tables are supported' + raise ValidationTiffError(msg) + + if self._tiffInfo.get('compression') == libtiff_ctypes.COMPRESSION_JPEG: + try: + self._getJpegTables() + except IOTiffError: + self._completeJpeg = True + + def _loadMetadata(self): + fields = [key.split('_', 1)[1].lower() for key in + dir(libtiff_ctypes.tiff_h) if key.startswith('TIFFTAG_')] + info = {} + for field in fields: + try: + value = self._tiffFile.GetField(field) + if value is not None: + info[field] = value + except TypeError as err: + self.logger.debug( + 'Loading field "%s" in directory number %d resulted in TypeError - "%s"', + field, self._directoryNum, err) + + for func in self.CoreFunctions[3:]: + if hasattr(self._tiffFile, func): + value = getattr(self._tiffFile, func)() + if value: + info[func.lower()] = value + self._tiffInfo = info + self._tileWidth = info.get('tilewidth') or info.get('imagewidth') + self._tileHeight = info.get('tilelength') or info.get('rowsperstrip') + self._imageWidth = info.get('imagewidth') + self._imageHeight = info.get('imagelength') + self._tilesAcross = (self._imageWidth + self._tileWidth - 1) // self._tileWidth + if not info.get('tilelength'): + self._stripsPerTile = int(max(1, math.ceil(256.0 / self._tileHeight))) + self._stripHeight = self._tileHeight + self._tileHeight = self._stripHeight * self._stripsPerTile + self._stripCount = int(math.ceil(float(self._imageHeight) / self._stripHeight)) + if info.get('orientation') in { + libtiff_ctypes.ORIENTATION_LEFTTOP, + libtiff_ctypes.ORIENTATION_RIGHTTOP, + libtiff_ctypes.ORIENTATION_RIGHTBOT, + libtiff_ctypes.ORIENTATION_LEFTBOT}: + self._imageWidth, self._imageHeight = self._imageHeight, self._imageWidth + self._tileWidth, self._tileHeight = self._tileHeight, self._tileWidth + self.parse_image_description(info.get('imagedescription', '')) + # From TIFF specification, tag 0x128, 2 is inches, 3 is centimeters. + units = {2: 25.4, 3: 10} + # If the resolution value is less than a threshold (100), don't use it, + # as it is probably just an inaccurate default. Values like 72dpi and + # 96dpi are common defaults, but so are small metric values, too. + if (not self._pixelInfo.get('mm_x') and info.get('xresolution') and + units.get(info.get('resolutionunit')) and + info.get('xresolution') >= 100): + self._pixelInfo['mm_x'] = units[info['resolutionunit']] / info['xresolution'] + if (not self._pixelInfo.get('mm_y') and info.get('yresolution') and + units.get(info.get('resolutionunit')) and + info.get('yresolution') >= 100): + self._pixelInfo['mm_y'] = units[info['resolutionunit']] / info['yresolution'] + if not self._pixelInfo.get('width') and self._imageWidth: + self._pixelInfo['width'] = self._imageWidth + if not self._pixelInfo.get('height') and self._imageHeight: + self._pixelInfo['height'] = self._imageHeight + + @methodcache(key=partial(strhash, '_getJpegTables')) + def _getJpegTables(self): + """ + Get the common JPEG Huffman-coding and quantization tables. + + See http://www.awaresystems.be/imaging/tiff/tifftags/jpegtables.html + for more information. + + :return: All Huffman and quantization tables, with JPEG table start + markers. + :rtype: bytes + :raises: Exception + """ + # TIFFTAG_JPEGTABLES uses (uint32*, void**) output arguments + # http://www.remotesensing.org/libtiff/man/TIFFGetField.3tiff.html + + tableSize = ctypes.c_uint32() + tableBuffer = ctypes.c_voidp() + + # Some versions of pylibtiff set an explicit list of argtypes for + # TIFFGetField. When this is done, we need to adjust them to match + # what is needed for our specific call. Other versions do not set + # argtypes, allowing any types to be passed without validation, in + # which case we do not need to alter the list. + if libtiff_ctypes.libtiff.TIFFGetField.argtypes: + libtiff_ctypes.libtiff.TIFFGetField.argtypes = \ + libtiff_ctypes.libtiff.TIFFGetField.argtypes[:2] + \ + [ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_void_p)] + if libtiff_ctypes.libtiff.TIFFGetField( + self._tiffFile, + libtiff_ctypes.TIFFTAG_JPEGTABLES, + ctypes.byref(tableSize), + ctypes.byref(tableBuffer)) != 1: + msg = 'Could not get JPEG Huffman / quantization tables' + raise IOTiffError(msg) + + tableSize = tableSize.value + tableBuffer = ctypes.cast(tableBuffer, ctypes.POINTER(ctypes.c_char)) + + if tableBuffer[:2] != b'\xff\xd8': + msg = 'Missing JPEG Start Of Image marker in tables' + raise IOTiffError(msg) + if tableBuffer[tableSize - 2:tableSize] != b'\xff\xd9': + msg = 'Missing JPEG End Of Image marker in tables' + raise IOTiffError(msg) + if tableBuffer[2:4] not in (b'\xff\xc4', b'\xff\xdb'): + msg = 'Missing JPEG Huffman or Quantization Table marker' + raise IOTiffError(msg) + + # Strip the Start / End Of Image markers + tableData = tableBuffer[2:tableSize - 2] + return tableData + + def _toTileNum(self, x, y, transpose=False): + """ + Get the internal tile number of a tile, from its row and column index. + + :param x: The column index of the desired tile. + :type x: int + :param y: The row index of the desired tile. + :type y: int + :param transpose: If true, transpose width and height + :type transpose: boolean + :return: The internal tile number of the desired tile. + :rtype int + :raises: InvalidOperationTiffError + """ + # TIFFCheckTile and TIFFComputeTile require pixel coordinates + if not transpose: + pixelX = int(x * self._tileWidth) + pixelY = int(y * self._tileHeight) + if x < 0 or y < 0 or pixelX >= self._imageWidth or pixelY >= self._imageHeight: + raise InvalidOperationTiffError( + 'Tile x=%d, y=%d does not exist' % (x, y)) + else: + pixelX = int(x * self._tileHeight) + pixelY = int(y * self._tileWidth) + if x < 0 or y < 0 or pixelX >= self._imageHeight or pixelY >= self._imageWidth: + raise InvalidOperationTiffError( + 'Tile x=%d, y=%d does not exist' % (x, y)) + # We had been using TIFFCheckTile, but with z=0 and sample=0, this is + # just a check that x, y is within the image + # if libtiff_ctypes.libtiff.TIFFCheckTile( + # self._tiffFile, pixelX, pixelY, 0, 0) == 0: + # raise InvalidOperationTiffError( + # 'Tile x=%d, y=%d does not exist' % (x, y)) + if self._tiffInfo.get('istiled'): + tileNum = pixelX // self._tileWidth + (pixelY // self._tileHeight) * self._tilesAcross + else: + # TIFFComputeStrip with sample=0 is just the row divided by the + # strip height + tileNum = pixelY // self._stripHeight + return tileNum + + @methodcache(key=partial(strhash, '_getTileByteCountsType')) + def _getTileByteCountsType(self): + """ + Get data type of the elements in the TIFFTAG_TILEBYTECOUNTS array. + + :return: The element type in TIFFTAG_TILEBYTECOUNTS. + :rtype: ctypes.c_uint64 or ctypes.c_uint16 + :raises: IOTiffError + """ + tileByteCountsFieldInfo = libtiff_ctypes.libtiff.TIFFFieldWithTag( + self._tiffFile, libtiff_ctypes.TIFFTAG_TILEBYTECOUNTS).contents + tileByteCountsLibtiffType = tileByteCountsFieldInfo.field_type + + if tileByteCountsLibtiffType == libtiff_ctypes.TIFFDataType.TIFF_LONG8: + return ctypes.c_uint64 + elif tileByteCountsLibtiffType == \ + libtiff_ctypes.TIFFDataType.TIFF_SHORT: + return ctypes.c_uint16 + else: + raise IOTiffError( + 'Invalid type for TIFFTAG_TILEBYTECOUNTS: %s' % tileByteCountsLibtiffType) + + def _getJpegFrameSize(self, tileNum): + """ + Get the file size in bytes of the raw encoded JPEG frame for a tile. + + :param tileNum: The internal tile number of the desired tile. + :type tileNum: int + :return: The size in bytes of the raw tile data for the desired tile. + :rtype: int + :raises: InvalidOperationTiffError or IOTiffError + """ + # TODO: is it worth it to memoize this? + + # TODO: remove this check, for additional speed + totalTileCount = libtiff_ctypes.libtiff.TIFFNumberOfTiles( + self._tiffFile).value + if tileNum >= totalTileCount: + msg = 'Tile number out of range' + raise InvalidOperationTiffError(msg) + + # pylibtiff treats the output of TIFFTAG_TILEBYTECOUNTS as a scalar + # uint32; libtiff's documentation specifies that the output will be an + # array of uint32; in reality and per the TIFF spec, the output is an + # array of either uint64 or unit16, so we need to call the ctypes + # interface directly to get this tag + # http://www.awaresystems.be/imaging/tiff/tifftags/tilebytecounts.html + + rawTileSizesType = self._getTileByteCountsType() + rawTileSizes = ctypes.POINTER(rawTileSizesType)() + + # Some versions of pylibtiff set an explicit list of argtypes for + # TIFFGetField. When this is done, we need to adjust them to match + # what is needed for our specific call. Other versions do not set + # argtypes, allowing any types to be passed without validation, in + # which case we do not need to alter the list. + if libtiff_ctypes.libtiff.TIFFGetField.argtypes: + libtiff_ctypes.libtiff.TIFFGetField.argtypes = \ + libtiff_ctypes.libtiff.TIFFGetField.argtypes[:2] + \ + [ctypes.POINTER(ctypes.POINTER(rawTileSizesType))] + if libtiff_ctypes.libtiff.TIFFGetField( + self._tiffFile, + libtiff_ctypes.TIFFTAG_TILEBYTECOUNTS, + ctypes.byref(rawTileSizes)) != 1: + msg = 'Could not get raw tile size' + raise IOTiffError(msg) + + # In practice, this will never overflow, and it's simpler to convert the + # long to an int + return int(rawTileSizes[tileNum]) + + def _getJpegFrame(self, tileNum, entire=False): # noqa + """ + Get the raw encoded JPEG image frame from a tile. + + :param tileNum: The internal tile number of the desired tile. + :type tileNum: int + :param entire: True to return the entire frame. False to strip off + container information. + :return: The JPEG image frame, including a JPEG Start Of Frame marker. + :rtype: bytes + :raises: InvalidOperationTiffError or IOTiffError + """ + # This raises an InvalidOperationTiffError if the tile doesn't exist + rawTileSize = self._getJpegFrameSize(tileNum) + if rawTileSize <= 0: + msg = 'No raw tile data' + raise IOTiffError(msg) + + frameBuffer = ctypes.create_string_buffer(rawTileSize) + + bytesRead = libtiff_ctypes.libtiff.TIFFReadRawTile( + self._tiffFile, tileNum, + frameBuffer, rawTileSize).value + if bytesRead == -1: + msg = 'Failed to read raw tile' + raise IOTiffError(msg) + elif bytesRead < rawTileSize: + msg = 'Buffer underflow when reading tile' + raise IOTiffError(msg) + elif bytesRead > rawTileSize: + # It's unlikely that this will ever occur, but incomplete reads will + # be checked for by looking for the JPEG end marker + msg = 'Buffer overflow when reading tile' + raise IOTiffError(msg) + if entire: + return frameBuffer.raw[:] + + if frameBuffer.raw[:2] != b'\xff\xd8': + msg = 'Missing JPEG Start Of Image marker in frame' + raise IOTiffError(msg) + if frameBuffer.raw[-2:] != b'\xff\xd9': + msg = 'Missing JPEG End Of Image marker in frame' + raise IOTiffError(msg) + if frameBuffer.raw[2:4] in (b'\xff\xc0', b'\xff\xc2'): + frameStartPos = 2 + else: + # VIPS may encode TIFFs with the quantization (but not Huffman) + # tables also at the start of every frame, so locate them for + # removal + # VIPS seems to prefer Baseline DCT, so search for that first + frameStartPos = frameBuffer.raw.find(b'\xff\xc0', 2, -2) + if frameStartPos == -1: + frameStartPos = frameBuffer.raw.find(b'\xff\xc2', 2, -2) + if frameStartPos == -1: + msg = 'Missing JPEG Start Of Frame marker' + raise IOTiffError(msg) + # If the photometric value is RGB and the JPEG component ids are just + # 0, 1, 2, change the component ids to R, G, B to ensure color space + # information is preserved. + if self._tiffInfo.get('photometric') == libtiff_ctypes.PHOTOMETRIC_RGB: + sof = frameBuffer.raw.find(b'\xff\xc0') + if sof == -1: + sof = frameBuffer.raw.find(b'\xff\xc2') + sos = frameBuffer.raw.find(b'\xff\xda') + if (sof >= frameStartPos and sos >= frameStartPos and + frameBuffer[sof + 2:sof + 4] == b'\x00\x11' and + frameBuffer[sof + 10:sof + 19:3] == b'\x00\x01\x02' and + frameBuffer[sos + 5:sos + 11:2] == b'\x00\x01\x02'): + for idx, val in enumerate(b'RGB'): + frameBuffer[sof + 10 + idx * 3] = val + frameBuffer[sos + 5 + idx * 2] = val + # Strip the Start / End Of Image markers + tileData = frameBuffer.raw[frameStartPos:-2] + return tileData + + def _getUncompressedTile(self, tileNum): + """ + Get an uncompressed tile or strip. + + :param tileNum: The internal tile or strip number of the desired tile + or strip. + :type tileNum: int + :return: the tile as a PIL 8-bit-per-channel images. + :rtype: PIL.Image + :raises: IOTiffError + """ + if self._tiffInfo.get('istiled'): + if not hasattr(self, '_uncompressedTileSize'): + with self._tileLock: + self._uncompressedTileSize = libtiff_ctypes.libtiff.TIFFTileSize( + self._tiffFile).value + tileSize = self._uncompressedTileSize + else: + with self._tileLock: + stripSize = libtiff_ctypes.libtiff.TIFFStripSize( + self._tiffFile).value + stripsCount = min(self._stripsPerTile, self._stripCount - tileNum) + tileSize = stripSize * self._stripsPerTile + tw, th = self._tileWidth, self._tileHeight + if self._tiffInfo.get('orientation') in { + libtiff_ctypes.ORIENTATION_LEFTTOP, + libtiff_ctypes.ORIENTATION_RIGHTTOP, + libtiff_ctypes.ORIENTATION_RIGHTBOT, + libtiff_ctypes.ORIENTATION_LEFTBOT}: + tw, th = th, tw + format = ( + self._tiffInfo.get('bitspersample'), + self._tiffInfo.get('sampleformat') if self._tiffInfo.get( + 'sampleformat') is not None else libtiff_ctypes.SAMPLEFORMAT_UINT) + image = np.empty((th, tw, self._tiffInfo.get('samplesperpixel', 1)), + dtype=_ctypesFormattbl[format]) + imageBuffer = image.ctypes.data_as(ctypes.POINTER(ctypes.c_char)) + if self._tiffInfo.get('istiled'): + with self._tileLock: + readSize = libtiff_ctypes.libtiff.TIFFReadEncodedTile( + self._tiffFile, tileNum, imageBuffer, tileSize) + else: + readSize = 0 + imageBuffer = ctypes.cast(imageBuffer, ctypes.POINTER(ctypes.c_char * 2)).contents + for stripNum in range(stripsCount): + with self._tileLock: + chunkSize = libtiff_ctypes.libtiff.TIFFReadEncodedStrip( + self._tiffFile, + tileNum + stripNum, + ctypes.byref(imageBuffer, stripSize * stripNum), + stripSize).value + if chunkSize <= 0: + msg = 'Read an unexpected number of bytes from an encoded strip' + raise IOTiffError(msg) + readSize += chunkSize + if readSize < tileSize: + ctypes.memset(ctypes.byref(imageBuffer, readSize), 0, tileSize - readSize) + readSize = tileSize + if readSize < tileSize: + raise IOTiffError( + 'Read an unexpected number of bytes from an encoded tile' if readSize >= 0 else + 'Failed to read from an encoded tile') + if (self._tiffInfo.get('samplesperpixel', 1) == 3 and + self._tiffInfo.get('photometric') == libtiff_ctypes.PHOTOMETRIC_YCBCR): + if self._tiffInfo.get('bitspersample') == 16: + image = np.floor_divide(image, 256).astype(np.uint8) + image = PIL.Image.fromarray(image, 'YCbCr') + image = np.array(image.convert('RGB')) + return image + + def _getTileRotated(self, x, y): + """ + Get a tile from a rotated TIF. This composites uncompressed tiles as + necessary and then rotates the result. + + :param x: The column index of the desired tile. + :param y: The row index of the desired tile. + :return: either a buffer with a JPEG or a PIL image. + """ + x0 = x * self._tileWidth + x1 = x0 + self._tileWidth + y0 = y * self._tileHeight + y1 = y0 + self._tileHeight + iw, ih = self._imageWidth, self._imageHeight + tw, th = self._tileWidth, self._tileHeight + transpose = False + if self._tiffInfo.get('orientation') in { + libtiff_ctypes.ORIENTATION_LEFTTOP, + libtiff_ctypes.ORIENTATION_RIGHTTOP, + libtiff_ctypes.ORIENTATION_RIGHTBOT, + libtiff_ctypes.ORIENTATION_LEFTBOT}: + x0, x1, y0, y1 = y0, y1, x0, x1 + iw, ih = ih, iw + tw, th = th, tw + transpose = True + if self._tiffInfo.get('orientation') in { + libtiff_ctypes.ORIENTATION_TOPRIGHT, + libtiff_ctypes.ORIENTATION_BOTRIGHT, + libtiff_ctypes.ORIENTATION_RIGHTTOP, + libtiff_ctypes.ORIENTATION_RIGHTBOT}: + x0, x1 = iw - x1, iw - x0 + if self._tiffInfo.get('orientation') in { + libtiff_ctypes.ORIENTATION_BOTRIGHT, + libtiff_ctypes.ORIENTATION_BOTLEFT, + libtiff_ctypes.ORIENTATION_RIGHTBOT, + libtiff_ctypes.ORIENTATION_LEFTBOT}: + y0, y1 = ih - y1, ih - y0 + tx0 = x0 // tw + tx1 = (x1 - 1) // tw + ty0 = y0 // th + ty1 = (y1 - 1) // th + tile = None + for ty in range(max(0, ty0), max(0, ty1 + 1)): + for tx in range(max(0, tx0), max(0, tx1 + 1)): + subtile = self._getUncompressedTile(self._toTileNum(tx, ty, transpose)) + if tile is None: + tile = np.zeros( + (th, tw) if len(subtile.shape) == 2 else + (th, tw, subtile.shape[2]), dtype=subtile.dtype) + stx, sty = tx * tw - x0, ty * th - y0 + if (stx >= tw or stx + subtile.shape[1] <= 0 or + sty >= th or sty + subtile.shape[0] <= 0): + continue + if stx < 0: + subtile = subtile[:, -stx:] + stx = 0 + if sty < 0: + subtile = subtile[-sty:, :] + sty = 0 + subtile = subtile[:min(subtile.shape[0], th - sty), + :min(subtile.shape[1], tw - stx)] + tile[sty:sty + subtile.shape[0], stx:stx + subtile.shape[1]] = subtile + if tile is None: + raise InvalidOperationTiffError( + 'Tile x=%d, y=%d does not exist' % (x, y)) + if self._tiffInfo.get('orientation') in { + libtiff_ctypes.ORIENTATION_BOTRIGHT, + libtiff_ctypes.ORIENTATION_BOTLEFT, + libtiff_ctypes.ORIENTATION_RIGHTBOT, + libtiff_ctypes.ORIENTATION_LEFTBOT}: + tile = tile[::-1, :] + if self._tiffInfo.get('orientation') in { + libtiff_ctypes.ORIENTATION_TOPRIGHT, + libtiff_ctypes.ORIENTATION_BOTRIGHT, + libtiff_ctypes.ORIENTATION_RIGHTTOP, + libtiff_ctypes.ORIENTATION_RIGHTBOT}: + tile = tile[:, ::-1] + if self._tiffInfo.get('orientation') in { + libtiff_ctypes.ORIENTATION_LEFTTOP, + libtiff_ctypes.ORIENTATION_RIGHTTOP, + libtiff_ctypes.ORIENTATION_RIGHTBOT, + libtiff_ctypes.ORIENTATION_LEFTBOT}: + tile = tile.transpose((1, 0) if len(tile.shape) == 2 else (1, 0, 2)) + return tile + + @property + def tileWidth(self): + """ + Get the pixel width of tiles. + + :return: The tile width in pixels. + :rtype: int + """ + return self._tileWidth + + @property + def tileHeight(self): + """ + Get the pixel height of tiles. + + :return: The tile height in pixels. + :rtype: int + """ + return self._tileHeight + + @property + def imageWidth(self): + return self._imageWidth + + @property + def imageHeight(self): + return self._imageHeight + + @property + def pixelInfo(self): + return self._pixelInfo + +
+[docs] + def getTile(self, x, y, asarray=False): + """ + Get the complete JPEG image from a tile. + + :param x: The column index of the desired tile. + :type x: int + :param y: The row index of the desired tile. + :type y: int + :param asarray: If True, read jpeg compressed images as arrays. + :type asarray: boolean + :return: either a buffer with a JPEG or a PIL image. + :rtype: bytes + :raises: InvalidOperationTiffError or IOTiffError + """ + if self._tiffInfo.get('orientation') not in { + libtiff_ctypes.ORIENTATION_TOPLEFT, + None}: + return self._getTileRotated(x, y) + # This raises an InvalidOperationTiffError if the tile doesn't exist + tileNum = self._toTileNum(x, y) + + if (not self._tiffInfo.get('istiled') or + self._tiffInfo.get('compression') not in { + libtiff_ctypes.COMPRESSION_JPEG, 33003, 33005, 34712} or + self._tiffInfo.get('bitspersample') != 8 or + self._tiffInfo.get('sampleformat') not in { + None, libtiff_ctypes.SAMPLEFORMAT_UINT} or + (asarray and self._tiffInfo.get('compression') not in {33003, 33005, 34712} and ( + self._tiffInfo.get('compression') != libtiff_ctypes.COMPRESSION_JPEG or + self._tiffInfo.get('photometric') != libtiff_ctypes.PHOTOMETRIC_YCBCR))): + return self._getUncompressedTile(tileNum) + + imageBuffer = io.BytesIO() + + if (self._tiffInfo.get('compression') == libtiff_ctypes.COMPRESSION_JPEG and + not getattr(self, '_completeJpeg', False)): + # Write JPEG Start Of Image marker + imageBuffer.write(b'\xff\xd8') + imageBuffer.write(self._getJpegTables()) + imageBuffer.write(self._getJpegFrame(tileNum)) + # Write JPEG End Of Image marker + imageBuffer.write(b'\xff\xd9') + return imageBuffer.getvalue() + # Get the whole frame, which is in a JPEG or JPEG 2000 format + frame = self._getJpegFrame(tileNum, True) + # For JP2K, see if we can convert it faster than PIL + if self._tiffInfo.get('compression') in {33003, 33005}: + try: + import openjpeg + + return openjpeg.decode(frame) + except Exception: + pass + # convert it to a PIL image + imageBuffer.write(frame) + image = PIL.Image.open(imageBuffer) + # Converting the image mode ensures that it gets loaded once and is in + # a form we expect. If this isn't done, then PIL can load the image + # multiple times, which sometimes throws an exception in PIL's JPEG + # 2000 module. + if image.mode != 'L': + image = image.convert('RGB') + else: + image.load() + return image
+ + +
+[docs] + def parse_image_description(self, meta=None): # noqa + self._pixelInfo = {} + self._embeddedImages = {} + + if not meta: + return + if not isinstance(meta, str): + meta = meta.decode(errors='ignore') + try: + parsed = json.loads(meta) + if isinstance(parsed, dict): + self._description_record = parsed + return True + except Exception: + pass + try: + xml = ElementTree.fromstring(meta) + except Exception: + if 'AppMag = ' in meta: + try: + self._pixelInfo = { + 'magnification': float(meta.split('AppMag = ')[1].split('|')[0].strip()), + } + self._pixelInfo['mm_x'] = self._pixelInfo['mm_y'] = float( + meta.split('|MPP = ', 1)[1].split('|')[0].strip()) * 0.001 + except Exception: + pass + return + try: + image = xml.find( + ".//DataObject[@ObjectType='DPScannedImage']") + columns = int(image.find(".//*[@Name='PIM_DP_IMAGE_COLUMNS']").text) + rows = int(image.find(".//*[@Name='PIM_DP_IMAGE_ROWS']").text) + spacing = [float(val.strip('"')) for val in image.find( + ".//*[@Name='DICOM_PIXEL_SPACING']").text.split()] + self._pixelInfo = { + 'width': columns, + 'height': rows, + 'mm_x': spacing[0], + 'mm_y': spacing[1], + } + except Exception: + pass + # Extract macro and label images + for image in xml.findall(".//*[@ObjectType='DPScannedImage']"): + try: + typestr = image.find(".//*[@Name='PIM_DP_IMAGE_TYPE']").text + datastr = image.find(".//*[@Name='PIM_DP_IMAGE_DATA']").text + except Exception: + continue + if not typestr or not datastr: + continue + typemap = { + 'LABELIMAGE': 'label', + 'MACROIMAGE': 'macro', + 'WSI': 'thumbnail', + } + self._embeddedImages[typemap.get(typestr, typestr.lower())] = datastr + try: + self._description_record = etreeToDict(xml) + except Exception: + pass + return True
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_tifffile.html b/_modules/large_image_source_tifffile.html new file mode 100644 index 000000000..13a8d3c52 --- /dev/null +++ b/_modules/large_image_source_tifffile.html @@ -0,0 +1,797 @@ + + + + + + large_image_source_tifffile — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_tifffile

+import json
+import logging
+import math
+import os
+import threading
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+import numpy as np
+
+import large_image
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import TILE_FORMAT_NUMPY, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource
+
+tifffile = None
+zarr = None
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+
+[docs] +class checkForMissingDataHandler(logging.Handler): +
+[docs] + def emit(self, record): + msg = record.getMessage() + if 'Missing data are zeroed' in msg or 'OME series expected ' in msg: + raise TileSourceError(record.getMessage())
+
+ + + +def _lazyImport(): + """ + Import the tifffile module. This is done when needed rather than in the + module initialization because it is slow. + """ + global tifffile + global zarr + + if tifffile is None: + try: + import tifffile + except ImportError: + msg = 'tifffile module not found.' + raise TileSourceError(msg) + if not hasattr(tifffile.TiffTag, 'dtype_name') or not hasattr(tifffile.TiffPage, 'aszarr'): + tifffile = None + msg = 'tifffile module is too old.' + raise TileSourceError(msg) + # The missing data handler consumes most warnings, but throws if a + # warning about missing data occurs + # The tifffile.tifffile logger is in older versions of tifffile + logging.getLogger('tifffile.tifffile').setLevel(logging.WARNING) + logging.getLogger('tifffile.tifffile').addHandler(checkForMissingDataHandler()) + logging.getLogger('tifffile').setLevel(logging.WARNING) + logging.getLogger('tifffile').addHandler(checkForMissingDataHandler()) + if zarr is None: + import zarr + + +
+[docs] +def et_findall(tag, text): + """ + Find all the child tags in an element tree that end with a specific string. + + :param tag: the tag to search. + :param text: the text to end with. + :returns: a list of tags. + """ + return [entry for entry in tag if entry.tag.endswith(text)]
+ + + +
+[docs] +class TifffileFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to files that the tifffile library can read. + """ + + cacheName = 'tilesource' + name = 'tifffile' + extensions = { + None: SourcePriority.LOW, + 'scn': SourcePriority.PREFERRED, + 'tif': SourcePriority.LOW, + 'tiff': SourcePriority.LOW, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'image/scn': SourcePriority.PREFERRED, + 'image/tiff': SourcePriority.LOW, + 'image/x-tiff': SourcePriority.LOW, + } + + # Fallback for non-tiled or oddly tiled sources + _tileSize = 512 + _minImageSize = 128 + _minTileSize = 128 + _singleTileSize = 1024 + _maxTileSize = 2048 + _minAssociatedImageSize = 64 + _maxAssociatedImageSize = 8192 + + def __init__(self, path, **kwargs): # noqa + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + self._largeImagePath = str(self._getLargeImagePath()) + + _lazyImport() + self.addKnownExtensions() + try: + self._tf = tifffile.TiffFile(self._largeImagePath) + except Exception: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + msg = 'File cannot be opened via tifffile.' + raise TileSourceError(msg) + maxseries, maxsamples = self._biggestSeries() + self.tileWidth = self.tileHeight = self._tileSize + s = self._tf.series[maxseries] + self._baseSeries = s + if len(s.levels) == 1: + self.tileWidth = self.tileHeight = self._singleTileSize + page = s.pages[0] + if ('TileWidth' in page.tags and + self._minTileSize <= page.tags['TileWidth'].value <= self._maxTileSize): + self.tileWidth = page.tags['TileWidth'].value + if ('TileLength' in page.tags and + self._minTileSize <= page.tags['TileLength'].value <= self._maxTileSize): + self.tileHeight = page.tags['TileLength'].value + if 'InterColorProfile' in page.tags: + self._iccprofiles = [page.tags['InterColorProfile'].value] + self.sizeX = s.shape[s.axes.index('X')] + self.sizeY = s.shape[s.axes.index('Y')] + self._mm_x = self._mm_y = None + try: + unit = {2: 25.4, 3: 10}[page.tags['ResolutionUnit'].value.real] + + if (page.tags['XResolution'].value[0] and page.tags['XResolution'].value[1] and ( + page.tags['XResolution'].value[0] / page.tags['XResolution'].value[1]) >= 100): + self._mm_x = (unit * page.tags['XResolution'].value[1] / + page.tags['XResolution'].value[0]) + if (page.tags['YResolution'].value[0] and page.tags['YResolution'].value[1] and ( + page.tags['YResolution'].value[0] / page.tags['YResolution'].value[1]) >= 100): + self._mm_y = (unit * page.tags['YResolution'].value[1] / + page.tags['YResolution'].value[0]) + except Exception: + pass + self._findMatchingSeries() + self.levels = int(max(1, math.ceil(math.log( + float(max(self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2)) + 1)) + self._findAssociatedImages() + for key in dir(self._tf): + if (key.startswith('is_') and hasattr(self, '_handle_' + key[3:]) and + getattr(self._tf, key)): + getattr(self, '_handle_' + key[3:])() + self._populatedLevels = len(self._baseSeries.levels) + # Some files have their axes listed in the wrong order. Try to access + # the lastmost pixel; if that fails, probably the axes and shape don't + # match the file (or the file is corrupted). + try: + self.getPixel(region={'left': self.sizeX - 1, 'top': self.sizeY - 1}, + frame=self.frames - 1) + except Exception: + msg = 'File cannot be opened via tifffile: axes and shape do not match access pattern.' + raise TileSourceError(msg) + + def _biggestSeries(self): + """ + Find the series with the most pixels. Use all series that have the + same dimensionality and resolution. They can differ in X, Y size. + + :returns: index of the largest series, number of pixels in a frame in + that series. + """ + maxseries = None + maxsamples = 0 + ex = 'no maximum series' + try: + for idx, s in enumerate(self._tf.series): + samples = math.prod(s.shape) + if samples > maxsamples and 'X' in s.axes and 'Y' in s.axes: + maxseries = idx + maxsamples = samples + except Exception as exc: + self.logger.debug('Cannot use tifffile: %r', exc) + ex = exc + maxseries = None + if maxseries is None: + raise TileSourceError( + 'File cannot be opened via tifffile source: %r' % ex) + return maxseries, maxsamples + + def _findMatchingSeries(self): + """ + Given a series in self._baseSeries, find other series that have the + same axes and shape except that they may different in width and height. + Store the results in self._series, _seriesShape, _framecount, and + _basis. + """ + base = self._baseSeries + page = base.pages[0] + self._series = [] + self._seriesShape = [] + for idx, s in enumerate(self._tf.series): + if s != base: + if s.name.lower() in {'label', 'macro', 'thumbnail', 'map'}: + continue + if 'P' in base.axes or s.axes != base.axes: + continue + if not all(base.axes[sidx] in 'YX' or sl == base.shape[sidx] + for sidx, sl in enumerate(s.shape)): + continue + skip = False + for tag in {'ResolutionUnit', 'XResolution', 'YResolution'}: + if (tag in page.tags) != (tag in s.pages[0].tags) or ( + tag in page.tags and + page.tags[tag].value != s.pages[0].tags[tag].value): + skip = True + if skip: + continue + if (s.shape[s.axes.index('X')] < min(self.sizeX, self._minImageSize) and + s.shape[s.axes.index('Y')] < min(self.sizeY, self._minImageSize)): + continue + self._series.append(idx) + self._seriesShape.append({ + 'sizeX': s.shape[s.axes.index('X')], 'sizeY': s.shape[s.axes.index('Y')]}) + self.sizeX = max(self.sizeX, s.shape[s.axes.index('X')]) + self.sizeY = max(self.sizeY, s.shape[s.axes.index('Y')]) + self._framecount = len(self._series) * math.prod(tuple( + 1 if base.axes[sidx] in 'YXS' else v for sidx, v in enumerate(base.shape))) + self._basis = {} + basis = 1 + if 'C' in base.axes: + self._basis['C'] = (1, base.axes.index('C'), base.shape[base.axes.index('C')]) + basis *= base.shape[base.axes.index('C')] + for axis in base.axes[::-1]: + if axis in 'CYXS': + continue + self._basis[axis] = (basis, base.axes.index(axis), base.shape[base.axes.index(axis)]) + basis *= base.shape[base.axes.index(axis)] + if len(self._series) > 1: + self._basis['P'] = (basis, -1, len(self._series)) + self._zarrlock = threading.RLock() + self._zarrcache = {} + + def _findAssociatedImages(self): + """ + Find associated images from unused pages and series. + """ + pagesInSeries = [p for s in self._tf.series for ll in s.pages.levels for p in ll.pages] + hashes = [p.hash for p in pagesInSeries if getattr(p, 'keyframe', None) is not None] + self._associatedImages = {} + for p in self._tf.pages: + if (p not in pagesInSeries and getattr(p, 'keyframe', None) is not None and + p.hash not in hashes and not len(set(p.axes) - set('YXS'))): + id = 'image_%s' % p.index + entry = {'page': p.index} + entry['width'] = p.shape[p.axes.index('X')] + entry['height'] = p.shape[p.axes.index('Y')] + if (id not in self._associatedImages and + max(entry['width'], entry['height']) <= self._maxAssociatedImageSize and + max(entry['width'], entry['height']) >= self._minAssociatedImageSize): + self._associatedImages[id] = entry + for sidx, s in enumerate(self._tf.series): + if sidx not in self._series and not len(set(s.axes) - set('YXS')): + id = 'series_%d' % sidx + if s.name and s.name.lower() not in self._associatedImages: + id = s.name.lower() + entry = {'series': sidx} + entry['width'] = s.shape[s.axes.index('X')] + entry['height'] = s.shape[s.axes.index('Y')] + if (id not in self._associatedImages and + max(entry['width'], entry['height']) <= self._maxAssociatedImageSize and + max(entry['width'], entry['height']) >= self._minAssociatedImageSize): + self._associatedImages[id] = entry + + def _handle_imagej(self): + try: + ijm = self._tf.pages[0].tags['IJMetadata'].value + if (ijm['Labels'] and len(ijm['Labels']) == self._framecount and + not getattr(self, '_channels', None)): + self._channels = ijm['Labels'] + except Exception: + pass + + def _handle_scn(self): # noqa + """ + For SCN files, parse the xml and possibly adjust how associated images + are labelled. + """ + import xml.etree.ElementTree + + import large_image.tilesource.utilities + + root = xml.etree.ElementTree.fromstring(self._tf.pages[0].description) + self._xml = large_image.tilesource.utilities.etreeToDict(root) + for collection in et_findall(root, 'collection'): + sizeX = collection.attrib.get('sizeX') + sizeY = collection.attrib.get('sizeY') + for supplementalImage in et_findall(collection, 'supplementalImage'): + name = supplementalImage.attrib.get('type', '').lower() + ifd = supplementalImage.attrib.get('ifd', '') + oldname = 'image_%s' % ifd + if (name and ifd and oldname in self._associatedImages and + name not in self._associatedImages): + self._associatedImages[name] = self._associatedImages[oldname] + self._associatedImages.pop(oldname, None) + for image in et_findall(collection, 'image'): + name = image.attrib.get('name', 'Unknown') + for view in et_findall(image, 'view'): + if (sizeX and view.attrib.get('sizeX') == sizeX and + sizeY and view.attrib.get('sizeY') == sizeY and + not int(view.attrib.get('offsetX')) and + not int(view.attrib.get('offsetY')) and + name.lower() in self._associatedImages and + 'macro' not in self._associatedImages): + self._associatedImages['macro'] = self._associatedImages[name.lower()] + self._associatedImages.pop(name.lower(), None) + if name != self._baseSeries.name: + continue + for scanSettings in et_findall(image, 'scanSettings'): + for objectiveSettings in et_findall(scanSettings, 'objectiveSettings'): + for objective in et_findall(objectiveSettings, 'objective'): + if not hasattr(self, '_magnification') and float(objective.text) > 0: + self._magnification = float(objective.text) + for channelSettings in et_findall(scanSettings, 'channelSettings'): + channels = {} + for channel in et_findall(channelSettings, 'channel'): + channels[int(channel.attrib.get('index', 0))] = ( + large_image.tilesource.utilities.etreeToDict(channel)['channel']) + self._channelInfo = channels + try: + self._channels = [ + channels.get(idx)['name'].split('|')[0] + for idx in range(len(channels))] + except Exception: + pass + + def _handle_svs(self): + """ + For SVS files, parse the magnification and pixel size. + """ + try: + meta = self._tf.pages[0].description + self._magnification = float(meta.split('AppMag = ')[1].split('|')[0].strip()) + self._mm_x = self._mm_y = float( + meta.split('|MPP = ', 1)[1].split('|')[0].strip()) * 0.001 + except Exception: + pass + + def _handle_ome(self): + """ + For OME Tiff, if we didn't parse the mangification elsewhere, try to + parse it here. + """ + import xml.etree.ElementTree + + import large_image.tilesource.utilities + + _omeUnitsToMeters = { + 'Ym': 1e24, + 'Zm': 1e21, + 'Em': 1e18, + 'Pm': 1e15, + 'Tm': 1e12, + 'Gm': 1e9, + 'Mm': 1e6, + 'km': 1e3, + 'hm': 1e2, + 'dam': 1e1, + 'm': 1, + 'dm': 1e-1, + 'cm': 1e-2, + 'mm': 1e-3, + '\u00b5m': 1e-6, + 'nm': 1e-9, + 'pm': 1e-12, + 'fm': 1e-15, + 'am': 1e-18, + 'zm': 1e-21, + 'ym': 1e-24, + '\u00c5': 1e-10, + } + + try: + root = xml.etree.ElementTree.fromstring(self._tf.pages[0].description) + self._xml = large_image.tilesource.utilities.etreeToDict(root) + except Exception: + return + try: + try: + base = self._xml['OME']['Image'][0]['Pixels'] + except Exception: + base = self._xml['OME']['Image']['Pixels'] + if self._mm_x is None and 'PhysicalSizeX' in base: + self._mm_x = ( + float(base['PhysicalSizeX']) * 1e3 * + _omeUnitsToMeters[base.get('PhysicalSizeXUnit', '\u00b5m')]) + if self._mm_y is None and 'PhysicalSizeY' in base: + self._mm_y = ( + float(base['PhysicalSizeY']) * 1e3 * + _omeUnitsToMeters[base.get('PhysicalSizeYUnit', '\u00b5m')]) + self._mm_x = self._mm_x or self._mm_y + self._mm_y = self._mm_y or self._mm_x + except Exception: + pass + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + mm_x = self._mm_x + mm_y = self._mm_y + # Estimate the magnification; we don't have a direct value + mag = 0.01 / mm_x if mm_x else None + return { + 'magnification': getattr(self, '_magnification', mag), + 'mm_x': mm_x, + 'mm_y': mm_y, + }
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + result = super().getMetadata() + if self._framecount > 1: + result['frames'] = frames = [] + for idx in range(self._framecount): + frame = {'Frame': idx} + for axis, (basis, _pos, count) in self._basis.items(): + if axis != 'I': + frame['Index' + (axis.upper() if axis.upper() != 'P' else 'XY')] = ( + idx // basis) % count + frames.append(frame) + self._addMetadataFrameInformation(result, getattr(self, '_channels', None)) + if any(v != self._seriesShape[0] for v in self._seriesShape): + result['SizesXY'] = self._seriesShape + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + result = {} + pages = [s.pages[0] for s in self._tf.series] + pagesInSeries = [p for s in self._tf.series for ll in s.pages.levels for p in ll.pages] + pages.extend([page for page in self._tf.pages if page not in pagesInSeries]) + for page in pages: + for tag in getattr(page, 'tags', []): + if (tag.dtype_name == 'ASCII' or ( + tag.dtype_name == 'BYTE' and isinstance(tag.value, dict))) and tag.value: + key = basekey = tag.name + suffix = 0 + while key in result: + if result[key] == tag.value: + break + suffix += 1 + key = '%s_%d' % (basekey, suffix) + result[key] = tag.value + if isinstance(result[key], dict): + result[key] = result[key].copy() + for subkey in list(result[key]): + try: + json.dumps(result[key][subkey]) + except Exception: + del result[key][subkey] + if hasattr(self, '_xml') and 'xml' not in result: + result.pop('ImageDescription', None) + result['xml'] = self._xml + if hasattr(self, '_channelInfo'): + result['channelInfo'] = self._channelInfo + result['tifffileKind'] = self._baseSeries.kind + return result
+ + +
+[docs] + def getAssociatedImagesList(self): + """ + Get a list of all associated images. + + :return: the list of image keys. + """ + return sorted(self._associatedImages)
+ + + def _getAssociatedImage(self, imageKey): + """ + Get an associated image in PIL format. + + :param imageKey: the key of the associated image. + :return: the image in PIL format or None. + """ + if imageKey in self._associatedImages: + entry = self._associatedImages[imageKey] + if 'page' in entry: + source = self._tf.pages[entry['page']] + else: + source = self._tf.series[entry['series']] + image = source.asarray() + axes = source.axes + if axes not in {'YXS', 'YX'}: + # rotate axes to YXS or YX + image = np.moveaxis(image, [ + source.axes.index(a) for a in 'YXS' if a in source.axes + ], range(len(source.axes))) + return large_image.tilesource.base._imageToPIL(image) + + def _nonemptyLevelsList(self, frame=0): + """ + Return a list of one value per level where the value is None if the + level does not exist in the file and any other value if it does. + + :param frame: the frame number. + :returns: a list of levels length. + """ + if frame is None: + frame = 0 + if hasattr(self, '_nonempty_levels_list') and frame in self._nonempty_levels_list: + return self._nonempty_levels_list[frame] + if len(self._series) > 1: + sidx = frame // self._basis['P'][0] + else: + sidx = 0 + nonempty = [None] * self.levels + nonempty[self.levels - 1] = True + series = self._tf.series[self._series[sidx]] + za, hasgbs = self._getZarrArray(series, sidx) + xidx = series.axes.index('X') + yidx = series.axes.index('Y') + for ll in range(1, len(series.levels)): + scale = round(math.log(max(za[0].shape[xidx] / za[ll].shape[xidx], + za[0].shape[yidx] / za[ll].shape[yidx])) / math.log(2)) + if 0 < scale < self.levels: + nonempty[self.levels - 1 - int(scale)] = True + if not hasattr(self, '_nonempty_levels_list'): + self._nonempty_levels_list = {} + self._nonempty_levels_list[frame] = nonempty + return nonempty + + def _getZarrArray(self, series, sidx): + with self._zarrlock: + if sidx not in self._zarrcache: + if len(self._zarrcache) > 10: + self._zarrcache = {} + za = zarr.open(series.aszarr(), mode='r') + hasgbs = hasattr(za[0], 'get_basic_selection') + if not hasgbs and math.prod(series.shape) < 256 * 1024 ** 2: + za = series.asarray() + self._zarrcache[sidx] = (za, hasgbs) + za, hasgbs = self._zarrcache[sidx] + return za, hasgbs + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame, self._framecount) + x0, y0, x1, y1, step = self._xyzToCorners(x, y, z) + if len(self._series) > 1: + sidx = frame // self._basis['P'][0] + else: + sidx = 0 + series = self._tf.series[self._series[sidx]] + za, hasgbs = self._getZarrArray(series, sidx) + xidx = series.axes.index('X') + yidx = series.axes.index('Y') + if hasgbs: + bza = za[0] + # we could cache this + for ll in range(len(series.levels) - 1, 0, -1): + scale = round(max(za[0].shape[xidx] / za[ll].shape[xidx], + za[0].shape[yidx] / za[ll].shape[yidx])) + if scale <= step and step // scale == step / scale: + bza = za[ll] + x0 //= scale + x1 //= scale + y0 //= scale + y1 //= scale + step //= scale + break + else: + bza = za + if step > 2 ** self._maxSkippedLevels: + tile = self._getTileFromEmptyLevel(x, y, z, **kwargs) + tile = large_image.tilesource.base._imageToNumpy(tile)[0] + else: + sel = [] + baxis = '' + for aidx, axis in enumerate(series.axes): + if axis == 'X': + sel.append(slice(x0, x1, step)) + baxis += 'X' + elif axis == 'Y': + sel.append(slice(y0, y1, step)) + baxis += 'Y' + elif axis == 'S': + sel.append(slice(series.shape[aidx])) + baxis += 'S' + else: + sel.append((frame // self._basis[axis][0]) % self._basis[axis][2]) + tile = bza[tuple(sel)] + # rotate + if baxis not in {'YXS', 'YX'}: + tile = np.moveaxis( + tile, [baxis.index(a) for a in 'YXS' if a in baxis], range(len(baxis))) + return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+ + +
+[docs] + @classmethod + def addKnownExtensions(cls): + if not hasattr(cls, '_addedExtensions'): + _lazyImport() + cls._addedExtensions = True + cls.extensions = cls.extensions.copy() + for ext in tifffile.TIFF.FILE_EXTENSIONS: + if ext not in cls.extensions: + cls.extensions[ext] = SourcePriority.IMPLICIT
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return TifffileFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return TifffileFileTileSource.canRead(*args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_tifffile/girder_source.html b/_modules/large_image_source_tifffile/girder_source.html new file mode 100644 index 000000000..94be6d072 --- /dev/null +++ b/_modules/large_image_source_tifffile/girder_source.html @@ -0,0 +1,150 @@ + + + + + + large_image_source_tifffile.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_tifffile.girder_source

+##############################################################################
+#  Copyright Kitware Inc.
+#
+#  Licensed under the Apache License, Version 2.0 ( the "License" );
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+##############################################################################
+
+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import TifffileFileTileSource
+
+
+
+[docs] +class TifffileGirderTileSource(TifffileFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with files that tifffile can read. + """ + + cacheName = 'tilesource' + name = 'tifffile'
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_vips.html b/_modules/large_image_source_vips.html new file mode 100644 index 000000000..e9b79c216 --- /dev/null +++ b/_modules/large_image_source_vips.html @@ -0,0 +1,798 @@ + + + + + + large_image_source_vips — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_vips

+import logging
+import math
+import os
+import threading
+import uuid
+from pathlib import Path
+
+import cachetools
+import numpy as np
+import pyvips
+
+from large_image import config
+from large_image.cache_util import LruCacheMetaclass, _cacheClearFuncs, methodcache
+from large_image.constants import (NEW_IMAGE_PATH_FLAG, TILE_FORMAT_NUMPY,
+                                   GValueToDtype, SourcePriority,
+                                   dtypeToGValue)
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource
+from large_image.tilesource.utilities import _imageToNumpy, _newFromFileLock
+
+logging.getLogger('pyvips').setLevel(logging.ERROR)
+
+# Default to ignoring files with no extension and some specific extensions.
+config.ConfigValues['source_vips_ignored_names'] = \
+    r'(^[^.]*|\.(yml|yaml|json|png|svs|mrxs))$'
+
+
+def _clearVipsCache():
+    old = pyvips.voperation.cache_get_max_files()
+    pyvips.voperation.cache_set_max_files(0)
+    pyvips.voperation.cache_set_max_files(old)
+    old = pyvips.voperation.cache_get_max()
+    pyvips.voperation.cache_set_max(0)
+    pyvips.voperation.cache_set_max(old)
+
+
+_cacheClearFuncs.append(_clearVipsCache)
+
+
+
+[docs] +class VipsFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to any libvips compatible file. + """ + + cacheName = 'tilesource' + name = 'vips' + extensions = { + None: SourcePriority.LOW, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + } + newPriority = SourcePriority.MEDIUM + + _tileSize = 256 + + def __init__(self, path, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + self.addKnownExtensions() + + if str(path).startswith(NEW_IMAGE_PATH_FLAG): + self._initNew(**kwargs) + return + self._largeImagePath = str(self._getLargeImagePath()) + self._editable = False + + config._ignoreSourceNames('vips', self._largeImagePath) + try: + with _newFromFileLock: + self._image = pyvips.Image.new_from_file(self._largeImagePath) + except pyvips.error.Error: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + msg = 'File cannot be opened via pyvips' + raise TileSourceError(msg) + self.sizeX = self._image.width + self.sizeY = self._image.height + self.tileWidth = self.tileHeight = self._tileSize + pages = 1 + if 'n-pages' in self._image.get_fields(): + pages = self._image.get('n-pages') + self._frames = [0] + for page in range(1, pages): + subInputPath = self._largeImagePath + '[page=%d]' % page + with _newFromFileLock: + subImage = pyvips.Image.new_from_file(subInputPath) + if subImage.width == self.sizeX and subImage.height == self.sizeY: + self._frames.append(page) + continue + if subImage.width * subImage.height < self.sizeX * self.sizeY: + continue + self._frames = [page] + self.sizeX = subImage.width + self.sizeY = subImage.height + try: + self._image.close() + except Exception: + pass + self._image = subImage + self.levels = int(max(1, math.ceil(math.log( + float(max(self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2)) + 1)) + if len(self._frames) > 1: + self._recentFrames = cachetools.LRUCache(maxsize=6) + self._frameLock = threading.RLock() + + def _initNew(self, **kwargs): + """ + Initialize the tile class for creating a new image. + """ + # Make unpickleable + self._unpickleable = True + self._largeImagePath = None + self._image = None + self.sizeX = self.sizeY = self.levels = 0 + self.tileWidth = self.tileHeight = self._tileSize + self._frames = [0] + self._cacheValue = str(uuid.uuid4()) + self._output = None + self._editable = True + self._bandRanges = None + self._addLock = threading.RLock() + +
+[docs] + def getState(self): + # Use the _cacheValue to avoid caching the source and tiles if we are + # creating something new. + if not hasattr(self, '_cacheValue'): + return super().getState() + return super().getState() + ',%s' % (self._cacheValue, )
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + result = {} + if not self._image: + return result + for key in self._image.get_fields(): + try: + result[key] = self._image.get(key) + except Exception: + pass + if len(self._frames) > 1: + result['frames'] = [] + for idx in range(1, len(self._frames)): + frameresult = {} + result['frames'].append(frameresult) + img = self._getFrameImage(idx) + for key in img.get_fields(): + try: + frameresult[key] = img.get(key) + except Exception: + pass + return result
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + result = super().getMetadata() + if len(self._frames) > 1: + result['frames'] = [{} for _ in self._frames] + self._addMetadataFrameInformation(result) + return result
+ + + def _getFrameImage(self, frame=0): + """ + Get the vips image associated with a specific frame. + + :param frame: the 0-based frame to get. + :returns: a vips image. + """ + if self._image is None and self._output: + self._outputToImage() + img = self._image + if frame > 0: + with self._frameLock: + if frame not in self._recentFrames: + subpath = self._largeImagePath + '[page=%d]' % self._frames[frame] + with _newFromFileLock: + img = pyvips.Image.new_from_file(subpath) + self._recentFrames[frame] = img + else: + img = self._recentFrames[frame] + return img + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + return { + 'mm_x': self.mm_x, + 'mm_y': self.mm_y, + 'magnification': 0.01 / self.mm_x if self.mm_x else None, + }
+ + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame, len(self._frames)) + img = self._getFrameImage(frame) + x0, y0, x1, y1, step = self._xyzToCorners(x, y, z) + tileimg = img.crop(x0, y0, x1 - x0, y1 - y0) + if step != 1: + tileimg = tileimg.resize(1.0 / step, kernel=pyvips.enums.Kernel.NEAREST, gap=0) + tile = np.ndarray( + buffer=tileimg.write_to_memory(), + dtype=GValueToDtype[tileimg.format], + shape=[tileimg.height, tileimg.width, tileimg.bands]) + return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+ + + def _checkEditable(self): + """ + Raise an exception if this is not an editable image. + """ + if not self._editable: + msg = 'Not an editable image' + raise TileSourceError(msg) + + def _updateBandRanges(self, tile): + """ + Given a 3-d numpy array, update the tracked band ranges. + + :param tile: a numpy array. + """ + amin = np.amin(tile, axis=(0, 1)) + amax = np.amax(tile, axis=(0, 1)) + if self._bandRanges is None: + self._bandRanges = { + 'min': amin, + 'max': amax, + } + else: + delta = len(self._bandRanges['min']) - len(amin) + if delta > 0: + amin = np.array(list(amin) + [0] * delta) + amax = np.array(list(amax) + [0] * delta) + elif delta < 0: + self._bandRanges['min'] = np.array(list(self._bandRanges['min']) + [0] * -delta) + self._bandRanges['max'] = np.array(list(self._bandRanges['max']) + [0] * -delta) + self._bandRanges = { + 'min': np.minimum(self._bandRanges['min'], amin), + 'max': np.maximum(self._bandRanges['max'], amax), + } + + def _addVipsImage(self, vimg, x=0, y=0): + """ + Add a vips image to the output image. + + :param vimg: a vips image. + :param x: location in destination for upper-left corner. + :param y: location in destination for upper-left corner. + """ + # Allow vips to persist the new tile to a temp file. Otherwise, it may + # try to hold all tiles in memory. + vimgTemp = pyvips.Image.new_temp_file('%s.v') + vimg.write(vimgTemp) + vimg = vimgTemp + with self._addLock: + if self._output is None: + self._output = { + 'images': [], + 'interp': vimg.interpretation, + 'bands': vimg.bands, + 'minx': None, + 'miny': None, + 'width': 0, + 'height': 0, + } + self._output['images'].append({'image': vimg, 'x': x, 'y': y}) + if (self._output['interp'] != vimg.interpretation and + self._output['interp'] != pyvips.Interpretation.MULTIBAND): + if vimg.interpretation in { + pyvips.Interpretation.MULTIBAND, pyvips.Interpretation.RGB}: + self._output['interp'] = vimg.interpretation + if vimg.interpretation == pyvips.Interpretation.RGB and self._output['bands'] == 2: + self._output['bands'] = 4 + self._output['bands'] = max(self._output['bands'], vimg.bands) + self._output['minx'] = min( + self._output['minx'] if self._output['minx'] is not None else x, x) + self._output['miny'] = min( + self._output['miny'] if self._output['miny'] is not None else y, y) + self._output['width'] = max(self._output['width'], x + vimg.width) + self._output['height'] = max(self._output['height'], y + vimg.height) + self._invalidateImage() + + def _invalidateImage(self): + """ + Invalidate the tile and class cache + """ + if self._output is not None: + self._image = None + w = self._output['width'] - min(0, self._output['minx']) + h = self._output['height'] - min(0, self._output['miny']) + w = max(self.minWidth or w, w) + h = max(self.minHeight or h, h) + self.sizeX = w + self.sizeY = h + self.levels = int(max(1, math.ceil(math.log( + float(max(self.sizeX, self.sizeY)) / self.tileWidth) / math.log(2)) + 1)) + self._cacheValue = str(uuid.uuid4()) + +
+[docs] + def addTile(self, tile, x=0, y=0, mask=None, interpretation=None): + """ + Add a numpy or image tile to the image, expanding the image as needed + to accommodate it. Note that x and y can be negative. If so, the + output image (and internal memory access of the image) will act as if + the 0, 0 point is the most negative position. Cropping is applied + after this offset. + + :param tile: a numpy array, PIL Image, vips image, or a binary string + with an image. The numpy array can have 2 or 3 dimensions. + :param x: location in destination for upper-left corner. + :param y: location in destination for upper-left corner. + :param mask: a 2-d numpy array (or 3-d if the last dimension is 1). + If specified, areas where the mask is false will not be altered. + :param interpretation: one of the pyvips.enums.Interpretation or 'L', + 'LA', 'RGB', "RGBA'. This defaults to RGB/RGBA for 3/4 channel + images and L/LA for 1/2 channels. The special value 'pixelmap' + will convert a 1 channel integer to a 3 channel RGB map. For + images which are not 1 or 3 bands with an optional alpha, specify + MULTIBAND. In this case, the mask option cannot be used. + """ + self._checkEditable() + if not isinstance(tile, pyvips.vimage.Image): + tile, mode = _imageToNumpy(tile) + interpretation = interpretation or mode + with self._addLock: + self._updateBandRanges(tile) + if interpretation == 'pixelmap': + with self._addLock: + self._interpretation = 'pixelmap' + tile = np.dstack(( + (tile % 256).astype(int), + (tile / 256).astype(int) % 256, + (tile / 65536).astype(int) % 256)).astype('B') + interpretation = pyvips.enums.Interpretation.RGB + if interpretation != pyvips.Interpretation.MULTIBAND and tile.shape[2] in {1, 3}: + newarr = np.zeros( + (tile.shape[0], tile.shape[1], tile.shape[2] + 1), dtype=tile.dtype) + newarr[:, :, :tile.shape[2]] = tile + newarr[:, :, -1] = min(np.iinfo( + tile.dtype).max, 255) if tile.dtype.kind in 'iu' else 255 + tile = newarr + if mask is not None: + if len(mask.shape) == 3: + mask = np.logical_or.reduce(mask, axis=2) + if tile.shape[2] in {2, 4}: + tile[:, :, -1] *= mask.astype(bool) + else: + msg = 'Cannot apply a mask if the source is not 1 or 3 channels.' + raise TileSourceError(msg) + if tile.dtype.char not in dtypeToGValue: + tile = tile.astype(float) + vimg = pyvips.Image.new_from_memory( + np.ascontiguousarray(tile).data, + tile.shape[1], tile.shape[0], tile.shape[2], + dtypeToGValue[tile.dtype.char]) + interpretation = interpretation if any( + v == interpretation for k, v in pyvips.enums.Interpretation.__dict__.items() + if not k.startswith('_')) else ( + pyvips.Interpretation.B_W if tile.shape[2] <= 2 else ( + pyvips.Interpretation.RGB if tile.shape[2] <= 4 else + pyvips.Interpretation.MULTIBAND)) + vimg = vimg.copy(interpretation=interpretation) + # The alpha channel is [0, 255] if we created it (which is true if the + # band range doesn't include it) + self._addVipsImage(vimg, x, y)
+ + + def _getVipsFormat(self): + """ + Get the recommended vips format for the output image based on the + band range and the interpretation. + + :returns: a vips BandFormat. + """ + bmin, bmax = min(self._bandRanges['min']), max(self._bandRanges['max']) + if getattr(self, '_interpretation', None) == 'pixelmap': + format = pyvips.enums.BandFormat.UCHAR + elif bmin >= -1 and bmax <= 1: + format = pyvips.enums.BandFormat.FLOAT + elif bmin >= 0 and bmax < 2 ** 8: + format = pyvips.enums.BandFormat.UCHAR + elif bmin >= 0 and bmax < 2 ** 16: + format = pyvips.enums.BandFormat.USHORT + elif bmin >= 0 and bmax < 2 ** 32: + format = pyvips.enums.BandFormat.UINT + elif bmin < 0 and bmin >= -(2 ** 7) and bmax < 2 ** 7: + format = pyvips.enums.BandFormat.CHAR + elif bmin < 0 and bmin >= -(2 ** 15) and bmax < 2 ** 15: + format = pyvips.enums.BandFormat.SHORT + elif bmin < 0 and bmin >= -(2 ** 31) and bmax < 2 ** 31: + format = pyvips.enums.BandFormat.INT + else: + format = pyvips.enums.BandFormat.FLOAT + return format + + def _outputToImage(self): + """ + Create a vips image that pipelines all of the pieces we have into a + single image. This makes an image that is large enough to hold all of + the pieces and is an appropriate datatype to represent the range of + values that are present. For pixelmaps, this will be RGB 8-bit. An + alpha channel is always included unless the intrepretation is + multichannel. + """ + with self._addLock: + bands = self._output['bands'] + if bands in {1, 3}: + bands += 1 + img = pyvips.Image.black(self.sizeX, self.sizeY, bands=bands) + if self.mm_x or self.mm_y: + img = img.copy( + xres=1.0 / (self.mm_x if self.mm_x else self._mm_y), + yres=1.0 / (self.mm_y if self.mm_y else self._mm_x)) + format = self._getVipsFormat() + if img.format != format: + img = img.cast(format) + baseimg = img.copy(interpretation=self._output['interp'], format=format) + + leaves = math.ceil(len(self._output['images']) ** (1. / 3)) + img = baseimg.copy() + trunk = baseimg.copy() + branch = baseimg.copy() + for idx, entry in enumerate(self._output['images']): + entryimage = entry['image'] + if img.format == 'float' and entry['image'].format == 'double': + entryimage = entryimage.cast(img.format) + branch = branch.composite( + entryimage, pyvips.BlendMode.OVER, + x=entry['x'] - min(0, self._output['minx']), + y=entry['y'] - min(0, self._output['miny'])) + if not ((idx + 1) % leaves) or idx + 1 == len(self._output['images']): + trunk = trunk.composite(branch, pyvips.BlendMode.OVER, x=0, y=0) + branch = baseimg.copy() + if not ((idx + 1) % (leaves * leaves)) or idx + 1 == len(self._output['images']): + img = img.composite(trunk, pyvips.BlendMode.OVER, x=0, y=0) + trunk = baseimg.copy() + self._image = img + +
+[docs] + def write(self, path, lossy=True, alpha=True, overwriteAllowed=True, vips_kwargs=None): + """ + Output the current image to a file. + + :param path: output path. + :param lossy: if false, emit a lossless file. + :param alpha: True if an alpha channel is allowed. + :param overwriteAllowed: if False, raise an exception if the output + path exists. + :param vips_kwargs: if not None, save the image using these kwargs to + the write_to_file function instead of the automatically chosen + ones. In this case, lossy is ignored and all vips options must be + manually specified. + """ + if not overwriteAllowed and os.path.exists(path): + raise TileSourceError('Output path exists (%s)' % str(path)) + with self._addLock: + img = self._getFrameImage(0) + # TODO: set image description: e.g., + # img.set_type( + # pyvips.GValue.gstr_type, 'image-description', + # json.dumps(dict(vars(opts), indexCount=found))) + if getattr(self, '_interpretation', None) == 'pixelmap': + img = img[:3] + elif (not alpha and getattr(self, '_output', {}).get( + 'interp') != pyvips.Interpretation.MULTIBAND): + img = img[:-1] + if self.crop: + x, y, w, h = self._crop + w = max(0, min(img.width - x, w)) + h = max(0, min(img.height - y, h)) + x = min(x, img.width) + y = min(y, img.height) + img = img.crop(x, y, w, h) + pathIsTiff = Path(path).suffix.lower() in {'.tif', '.tiff'} + pixels = img.width * img.height + if vips_kwargs is not None or not pathIsTiff: + img.write_to_file(path, **(vips_kwargs or {})) + elif not lossy: + img.write_to_file( + path, tile_width=self.tileWidth, tile_height=self.tileHeight, + tile=True, pyramid=True, bigtiff=pixels >= 2 * 1024 ** 3, + region_shrink='nearest', compression='lzw', predictor='horizontal') + else: + img.write_to_file( + path, tile_width=self.tileWidth, tile_height=self.tileHeight, + tile=True, pyramid=True, bigtiff=pixels >= 2 * 1024 ** 3, + compression='jpeg', Q=90)
+ + + @property + def crop(self): + """ + Crop only applies to the output file, not the internal data access. + + It consists of x, y, w, h in pixels. + """ + return getattr(self, '_crop', None) + + @crop.setter + def crop(self, value): + self._checkEditable() + if value is None: + self._crop = None + return + x, y, w, h = value + x = int(x) + y = int(y) + w = int(w) + h = int(h) + if x < 0 or y < 0 or w <= 0 or h <= 0: + msg = 'Crop must have non-negative x, y and positive w, h' + raise TileSourceError(msg) + self._crop = (x, y, w, h) + + @property + def minWidth(self): + return getattr(self, '_minWidth', None) + + @minWidth.setter + def minWidth(self, value): + self._checkEditable() + value = int(value) if value is not None else None + if value is not None and value <= 0: + msg = 'minWidth must be positive or None' + raise TileSourceError(msg) + if value != getattr(self, '_minWidth', None): + self._minWidth = value + self._invalidateImage() + + @property + def minHeight(self): + return getattr(self, '_minHeight', None) + + @minHeight.setter + def minHeight(self, value): + self._checkEditable() + value = int(value) if value is not None else None + if value is not None and value <= 0: + msg = 'minHeight must be positive or None' + raise TileSourceError(msg) + if value != getattr(self, '_minHeight', None): + self._minHeight = value + self._invalidateImage() + + @property + def mm_x(self): + if getattr(self, '_mm_x', None): + return self._mm_x + xres = 0 + if self._image: + xres = self._image.get('xres') or 0 + return 1.0 / xres if xres and xres != 1 else None + + @mm_x.setter + def mm_x(self, value): + self._checkEditable() + value = float(value) if value is not None else None + if value is not None and value <= 0: + msg = 'mm_x must be positive or None' + raise TileSourceError(msg) + if value != getattr(self, '_minHeight', None): + self._mm_x = value + self._invalidateImage() + + @property + def mm_y(self): + if getattr(self, '_mm_y', None): + return self._mm_y + yres = 0 + if self._image: + yres = self._image.get('yres') or 0 + return 1.0 / yres if yres and yres != 1 else None + + @mm_y.setter + def mm_y(self, value): + self._checkEditable() + value = float(value) if value is not None else None + if value is not None and value <= 0: + msg = 'mm_y must be positive or None' + raise TileSourceError(msg) + if value != getattr(self, '_minHeight', None): + self._mm_y = value + self._invalidateImage() + + @property + def bandRanges(self): + return getattr(self, '_bandRanges', None) + + @property + def bandFormat(self): + if not self._editable: + return self._image.format + return self._getVipsFormat() + # TODO: specify bit depth / bandFormat explicitly + + @property + def origin(self): + if not self._editable: + return {'x': 0, 'y': 0} + return {'x': min(0, self._output['minx'] or 0), + 'y': min(0, self._output['miny'] or 0)} + +
+[docs] + @classmethod + def addKnownExtensions(cls): + if not hasattr(cls, '_addedExtensions'): + cls._addedExtensions = True + cls.extensions = cls.extensions.copy() + for dotext in pyvips.base.get_suffixes(): + ext = dotext.lstrip('.') + if ext not in cls.extensions: + cls.extensions[ext] = SourcePriority.IMPLICIT
+
+ + + +
+[docs] +def open(*args, **kwargs): + """Create an instance of the module class.""" + return VipsFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """Check if an input can be read by the module class.""" + return VipsFileTileSource.canRead(*args, **kwargs)
+ + + +
+[docs] +def new(*args, **kwargs): + """ + Create a new image, collecting the results from patches of numpy arrays or + smaller images. + """ + return VipsFileTileSource(NEW_IMAGE_PATH_FLAG + str(uuid.uuid4()), *args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_vips/girder_source.html b/_modules/large_image_source_vips/girder_source.html new file mode 100644 index 000000000..f0d64a944 --- /dev/null +++ b/_modules/large_image_source_vips/girder_source.html @@ -0,0 +1,137 @@ + + + + + + large_image_source_vips.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_vips.girder_source

+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import VipsFileTileSource
+
+
+
+[docs] +class VipsGirderTileSource(VipsFileTileSource, GirderTileSource): + """ + Vips large_image tile source for Girder. + """ + + cacheName = 'tilesource' + name = 'vips' + + # vips uses extensions and adjacent files for some formats + _mayHaveAdjacentFiles = True
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_zarr.html b/_modules/large_image_source_zarr.html new file mode 100644 index 000000000..f4c2e406d --- /dev/null +++ b/_modules/large_image_source_zarr.html @@ -0,0 +1,1194 @@ + + + + + + large_image_source_zarr — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_zarr

+import math
+import multiprocessing
+import os
+import shutil
+import tempfile
+import threading
+import uuid
+import warnings
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+from pathlib import Path
+
+import numpy as np
+import packaging.version
+
+import large_image
+from large_image.cache_util import LruCacheMetaclass, methodcache
+from large_image.constants import NEW_IMAGE_PATH_FLAG, TILE_FORMAT_NUMPY, SourcePriority
+from large_image.exceptions import TileSourceError, TileSourceFileNotFoundError
+from large_image.tilesource import FileTileSource
+from large_image.tilesource.resample import ResampleMethod, downsampleTileHalfRes
+from large_image.tilesource.utilities import _imageToNumpy, nearPowerOfTwo
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+zarr = None
+
+
+def _lazyImport():
+    """
+    Import the zarr module.  This is done when needed rather than in the module
+    initialization because it is slow.
+    """
+    global zarr
+
+    if zarr is None:
+        try:
+            import zarr
+
+            warnings.filterwarnings('ignore', category=FutureWarning, module='.*zarr.*')
+        except ImportError:
+            msg = 'zarr module not found.'
+            raise TileSourceError(msg)
+
+
+
+[docs] +class ZarrFileTileSource(FileTileSource, metaclass=LruCacheMetaclass): + """ + Provides tile access to files that the zarr library can read. + """ + + cacheName = 'tilesource' + name = 'zarr' + extensions = { + None: SourcePriority.LOW, + 'zarr': SourcePriority.PREFERRED, + 'zgroup': SourcePriority.PREFERRED, + 'zattrs': SourcePriority.PREFERRED, + 'zarray': SourcePriority.PREFERRED, + 'db': SourcePriority.MEDIUM, + 'zip': SourcePriority.LOWER, + } + mimeTypes = { + None: SourcePriority.FALLBACK, + 'application/zip+zarr': SourcePriority.PREFERRED, + 'application/vnd+zarr': SourcePriority.PREFERRED, + 'application/x-zarr': SourcePriority.PREFERRED, + } + newPriority = SourcePriority.HIGH + + _tileSize = 512 + _minTileSize = 128 + _maxTileSize = 1024 + _minAssociatedImageSize = 64 + _maxAssociatedImageSize = 8192 + + def __init__(self, path, **kwargs): + """ + Initialize the tile class. See the base class for other available + parameters. + + :param path: a filesystem path for the tile source. + """ + super().__init__(path, **kwargs) + + _lazyImport() + if str(path).startswith(NEW_IMAGE_PATH_FLAG): + self._initNew(path, **kwargs) + else: + self._initOpen(**kwargs) + self._tileLock = threading.RLock() + + def _initOpen(self, **kwargs): + self._largeImagePath = str(self._getLargeImagePath()) + self._zarr = None + self._editable = False + if not os.path.isfile(self._largeImagePath) and '//:' not in self._largeImagePath: + raise TileSourceFileNotFoundError(self._largeImagePath) from None + try: + self._zarr = zarr.open(zarr.SQLiteStore(self._largeImagePath), mode='r') + except Exception: + try: + self._zarr = zarr.open(self._largeImagePath, mode='r') + except Exception: + if os.path.basename(self._largeImagePath) in {'.zgroup', '.zattrs', '.zarray'}: + try: + self._zarr = zarr.open(os.path.dirname(self._largeImagePath), mode='r') + except Exception: + pass + if self._zarr is None: + if not os.path.isfile(self._largeImagePath): + raise TileSourceFileNotFoundError(self._largeImagePath) from None + msg = 'File cannot be opened via zarr.' + raise TileSourceError(msg) + try: + self._validateZarr() + except TileSourceError: + raise + except Exception: + msg = 'File cannot be opened -- not an OME NGFF file or understandable zarr file.' + raise TileSourceError(msg) + + def _initNew(self, path, **kwargs): + """ + Initialize the tile class for creating a new image. + """ + self._tempdir = Path(tempfile.gettempdir(), path) + self._created = False + if not self._tempdir.exists(): + self._created = True + self._zarr_store = zarr.DirectoryStore(str(self._tempdir)) + self._zarr = zarr.open(self._zarr_store, mode='a') + self._largeImagePath = None + self._dims = {} + self.sizeX = self.sizeY = self.levels = 0 + self.tileWidth = self.tileHeight = self._tileSize + self._cacheValue = str(uuid.uuid4()) + self._output = None + self._editable = True + self._bandRanges = None + self._threadLock = threading.RLock() + self._processLock = multiprocessing.Lock() + self._framecount = 0 + self._mm_x = 0 + self._mm_y = 0 + self._channelNames = [] + self._channelColors = [] + self._imageDescription = None + self._levels = [] + self._associatedImages = {} + + def __del__(self): + if not hasattr(self, '_derivedSource'): + try: + self._zarr.close() + except Exception: + pass + try: + if self._created: + shutil.rmtree(self._tempdir) + except Exception: + pass + + def _checkEditable(self): + """ + Raise an exception if this is not an editable image. + """ + if not self._editable: + msg = 'Not an editable image' + raise TileSourceError(msg) + + def _getGeneralAxes(self, arr): + """ + Examine a zarr array an guess what the axes are. We assume the two + maximal dimensions are y, x. Then, if there is a dimension that is 3 + or 4 in length, it is channels. If there is more than one other that + is not 1, we don't know how it is sorted, so we will fail. + + :param arr: a zarr array. + :return: a dictionary of axes with the axis as the key and the index + within the array axes as the value. + """ + shape = arr.shape + maxIndex = shape.index(max(shape)) + secondMaxIndex = shape.index(max(x for idx, x in enumerate(shape) if idx != maxIndex)) + axes = { + 'x': max(maxIndex, secondMaxIndex), + 'y': min(maxIndex, secondMaxIndex), + } + for idx, val in enumerate(shape): + if idx not in axes.values() and val == 4 and 'c' not in axes: + axes['c'] = idx + if idx not in axes.values() and val == 3: + axes['c'] = idx + for idx, val in enumerate(shape): + if idx not in axes.values() and val > 1: + if 'f' in axes: + msg = 'Too many large axes' + raise TileSourceError(msg) + axes['f'] = idx + return axes + + def _scanZarrArray(self, group, arr, results): + """ + Scan a zarr array and determine if is the maximal dimension array we + can read. If so, update a results dictionary with the information. If + it is the shape as a previous maximum, append it as a multi-series set. + If not, and it is small enough, add it to a list of possible associated + images. + + :param group: a zarr group; the parent of the array. + :param arr: a zarr array. + :param results: a dictionary to store the results. 'best' contains a + tuple that is used to find the maximum size array, preferring ome + arrays, then total pixels, then channels. 'is_ome' is a boolean. + 'series' is a list of the found groups and arrays that match the + best criteria. 'axes' and 'channels' are from the best array. + 'associated' is a list of all groups and arrays that might be + associated images. These have to be culled for the actual groups + used in the series. + """ + attrs = group.attrs.asdict() if group is not None else {} + min_version = packaging.version.Version('0.4') + is_ome = ( + isinstance(attrs.get('multiscales', None), list) and + 'omero' in attrs and + isinstance(attrs['omero'], dict) and + all(isinstance(m, dict) for m in attrs['multiscales']) and + all(packaging.version.Version(m['version']) >= min_version + for m in attrs['multiscales'] if 'version' in m)) + channels = None + if is_ome: + axes = {axis['name']: idx for idx, axis in enumerate( + attrs['multiscales'][0]['axes'])} + if isinstance(attrs['omero'].get('channels'), list): + channels = [channel['label'] for channel in attrs['omero']['channels']] + if all(channel.startswith('Channel ') for channel in channels): + channels = None + else: + try: + axes = self._getGeneralAxes(arr) + except TileSourceError: + return + if 'x' not in axes or 'y' not in axes: + return + check = (is_ome, math.prod(arr.shape), channels is not None, + tuple(axes.keys()), tuple(channels) if channels else ()) + if results['best'] is None or check > results['best']: + results['best'] = check + results['series'] = [(group, arr)] + results['is_ome'] = is_ome + results['axes'] = axes + results['channels'] = channels + elif check == results['best']: + results['series'].append((group, arr)) + if not any(group is g for g, _ in results['associated']): + axes = {k: v for k, v in axes.items() if arr.shape[axes[k]] > 1} + if (len(axes) <= 3 and + self._minAssociatedImageSize <= arr.shape[axes['x']] <= + self._maxAssociatedImageSize and + self._minAssociatedImageSize <= arr.shape[axes['y']] <= + self._maxAssociatedImageSize and + (len(axes) == 2 or ('c' in axes and arr.shape[axes['c']] in {1, 3, 4}))): + results['associated'].append((group, arr)) + + def _scanZarrGroup(self, group, results=None): + """ + Scan a zarr group for usable arrays. + + :param group: a zarr group + :param results: a results dicitionary, updated. + :returns: the results dictionary. + """ + if results is None: + results = {'best': None, 'series': [], 'associated': []} + if isinstance(group, zarr.core.Array): + self._scanZarrArray(None, group, results) + return results + for val in group.values(): + if isinstance(val, zarr.core.Array): + self._scanZarrArray(group, val, results) + elif isinstance(val, zarr.hierarchy.Group): + results = self._scanZarrGroup(val, results) + return results + + def _zarrFindLevels(self): + """ + Find usable multi-level images. This checks that arrays are nearly a + power of two. This updates self._levels and self._populatedLevels. + self._levels is an array the same length as the number of series, each + entry of which is an array of the number of conceptual tile levels + where each entry of that is either the zarr array that can be used to + get pixels or None if it is not populated. + """ + levels = [[None] * self.levels for _ in self._series] + baseGroup, baseArray = self._series[0] + for idx, (_, arr) in enumerate(self._series): + levels[idx][0] = arr + arrs = [[arr for _, arr in s.arrays()] if s is not None else [a] for s, a in self._series] + for idx, arr in enumerate(arrs[0]): + if any(idx >= len(sarrs) for sarrs in arrs[1:]): + break + if any(arr.shape != sarrs[idx].shape for sarrs in arrs[1:]): + continue + if (nearPowerOfTwo(self.sizeX, arr.shape[self._axes['x']]) and + nearPowerOfTwo(self.sizeY, arr.shape[self._axes['y']])): + level = int(round(math.log(self.sizeX / arr.shape[self._axes['x']]) / math.log(2))) + if level < self.levels and levels[0][level] is None: + for sidx in range(len(self._series)): + levels[sidx][level] = arrs[sidx][idx] + self._levels = levels + self._populatedLevels = len([l for l in self._levels[0] if l is not None]) + # TODO: check for inefficient file and raise warning + + def _getScale(self): + """ + Get the scale values from the ome metadata and populate the class + values for _mm_x and _mm_y. + """ + unit = {'micrometer': 1e-3, 'millimeter': 1, 'meter': 1e3} + self._mm_x = self._mm_y = None + baseGroup, baseArray = self._series[0] + try: + ms = baseGroup.attrs.asdict()['multiscales'][0] + self._mm_x = ms['datasets'][0]['coordinateTransformations'][0][ + 'scale'][self._axes['x']] * unit[ms['axes'][self._axes['x']]['unit']] + self._mm_y = ms['datasets'][0]['coordinateTransformations'][0][ + 'scale'][self._axes['y']] * unit[ms['axes'][self._axes['y']]['unit']] + except Exception: + pass + + def _validateZarr(self): + """ + Validate that we can read tiles from the zarr parent group in + self._zarr. Set up the appropriate class variables. + """ + if self._editable and hasattr(self, '_axes'): + self._writeInternalMetadata() + found = self._scanZarrGroup(self._zarr) + if found['best'] is None: + msg = 'No data array that can be used.' + raise TileSourceError(msg) + self._series = found['series'] + baseGroup, baseArray = self._series[0] + self._is_ome = found['is_ome'] + self._axes = {k.lower(): v for k, v in found['axes'].items() if baseArray.shape[v] > 1} + if len(self._series) > 1 and 'xy' in self._axes: + msg = 'Conflicting xy axis data.' + raise TileSourceError(msg) + self._channels = found['channels'] + self._associatedImages = { + g.name.replace('/', ''): (g, a) + for g, a in found['associated'] if not any(g is gb for gb, _ in self._series) + } + self.sizeX = baseArray.shape[self._axes['x']] + self.sizeY = baseArray.shape[self._axes['y']] + self.tileWidth = ( + baseArray.chunks[self._axes['x']] + if self._minTileSize <= baseArray.chunks[self._axes['x']] <= self._maxTileSize else + self._tileSize) + self.tileHeight = ( + baseArray.chunks[self._axes['y']] + if self._minTileSize <= baseArray.chunks[self._axes['y']] <= self._maxTileSize else + self._tileSize) + # If we wanted to require equal tile width and height: + # self.tileWidth = self.tileHeight = self._tileSize + # if (baseArray.chunks[self._axes['x']] == baseArray.chunks[self._axes['y']] and + # self._minTileSize <= baseArray.chunks[self._axes['x']] <= self._maxTileSize): + # self.tileWidth = self.tileHeight = baseArray.chunks[self._axes['x']] + self.levels = int(max(1, math.ceil(math.log(max( + self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1)) + self._dtype = baseArray.dtype + self._bandCount = 1 + if ('c' in self._axes and 's' not in self._axes and not self._channels and + baseArray.shape[self._axes.get('c')] in {1, 3, 4}): + self._bandCount = baseArray.shape[self._axes['c']] + self._axes['s'] = self._axes.pop('c') + elif 's' in self._axes: + self._bandCount = baseArray.shape[self._axes['s']] + self._zarrFindLevels() + self._getScale() + stride = 1 + self._strides = {} + self._axisCounts = {} + for _, k in sorted((-'tzc'.index(k) if k in 'tzc' else 1, k) + for k in self._axes if k not in 'xys'): + self._strides[k] = stride + self._axisCounts[k] = baseArray.shape[self._axes[k]] + stride *= baseArray.shape[self._axes[k]] + if len(self._series) > 1: + self._strides['xy'] = stride + self._axisCounts['xy'] = len(self._series) + stride *= len(self._series) + self._framecount = stride + + def _nonemptyLevelsList(self, frame=0): + """ + Return a list of one value per level where the value is None if the + level does not exist in the file and any other value if it does. + + :param frame: the frame number. + :returns: a list of levels length. + """ + return [True if l is not None else None for l in self._levels[0][::-1]] + +
+[docs] + def getNativeMagnification(self): + """ + Get the magnification at a particular level. + + :return: magnification, width of a pixel in mm, height of a pixel in mm. + """ + mm_x = self._mm_x + mm_y = self._mm_y + # Estimate the magnification; we don't have a direct value + mag = 0.01 / mm_x if mm_x else None + return { + 'magnification': getattr(self, '_magnification', mag), + 'mm_x': mm_x, + 'mm_y': mm_y, + }
+ + +
+[docs] + def getState(self): + # Use the _cacheValue to avoid caching the source and tiles if we are + # creating something new. + if not hasattr(self, '_cacheValue'): + return super().getState() + return super().getState() + ',%s' % (self._cacheValue, )
+ + +
+[docs] + def getMetadata(self): + """ + Return a dictionary of metadata containing levels, sizeX, sizeY, + tileWidth, tileHeight, magnification, mm_x, mm_y, and frames. + + :returns: metadata dictionary. + """ + if self._levels is None: + self._validateZarr() + result = super().getMetadata() + if self._framecount > 1: + result['frames'] = frames = [] + for idx in range(self._framecount): + frame = {'Frame': idx} + for axis in self._strides: + frame['Index' + axis.upper()] = ( + idx // self._strides[axis]) % self._axisCounts[axis] + frames.append(frame) + self._addMetadataFrameInformation(result, getattr(self, '_channels', None)) + return result
+ + +
+[docs] + def getInternalMetadata(self, **kwargs): + """ + Return additional known metadata about the tile source. Data returned + from this method is not guaranteed to be in any particular format or + have specific values. + + :returns: a dictionary of data or None. + """ + if self._editable: + self._writeInternalMetadata() + result = {} + result['zarr'] = { + 'base': self._zarr.attrs.asdict(), + } + try: + result['zarr']['main'] = self._series[0][0].attrs.asdict() + except Exception: + pass + return result
+ + +
+[docs] + def getAssociatedImagesList(self): + """ + Get a list of all associated images. + + :return: the list of image keys. + """ + return list(self._associatedImages.keys())
+ + + def _getAssociatedImage(self, imageKey): + """ + Get an associated image in PIL format. + + :param imageKey: the key of the associated image. + :return: the image in PIL format or None. + """ + if imageKey not in self._associatedImages: + return + group, arr = self._associatedImages[imageKey] + axes = self._getGeneralAxes(arr) + trans = [idx for idx in range(len(arr.shape)) + if idx not in axes.values()] + [axes['y'], axes['x']] + if 'c' in axes or 's' in axes: + trans.append(axes.get('c', axes.get('s'))) + with self._tileLock: + img = np.transpose(arr, trans).squeeze() + if len(img.shape) == 2: + img.expand_dims(axis=2) + return large_image.tilesource.base._imageToPIL(img) + +
+[docs] + @methodcache() + def getTile(self, x, y, z, pilImageAllowed=False, numpyAllowed=False, **kwargs): + if self._levels is None: + self._validateZarr() + + frame = self._getFrame(**kwargs) + self._xyzInRange(x, y, z, frame, self._framecount) + x0, y0, x1, y1, step = self._xyzToCorners(x, y, z) + sidx = 0 if len(self._series) <= 1 else frame // self._strides['xy'] + targlevel = self.levels - 1 - z + while targlevel and self._levels[sidx][targlevel] is None: + targlevel -= 1 + arr = self._levels[sidx][targlevel] + scale = int(2 ** targlevel) + x0 //= scale + y0 //= scale + x1 //= scale + y1 //= scale + step //= scale + if step > 2 ** self._maxSkippedLevels: + tile = self._getTileFromEmptyLevel(x, y, z, **kwargs) + tile = large_image.tilesource.base._imageToNumpy(tile)[0] + else: + idx = [slice(None) for _ in arr.shape] + idx[self._axes['x']] = slice(x0, x1, step) + idx[self._axes['y']] = slice(y0, y1, step) + for key in self._axes: + if key in self._strides: + pos = (frame // self._strides[key]) % self._axisCounts[key] + idx[self._axes[key]] = slice(pos, pos + 1) + trans = [idx for idx in range(len(arr.shape)) + if idx not in {self._axes['x'], self._axes['y'], + self._axes.get('s', self._axes['x'])}] + squeezeCount = len(trans) + trans += [self._axes['y'], self._axes['x']] + if 's' in self._axes: + trans.append(self._axes['s']) + with self._tileLock: + tile = arr[tuple(idx)] + tile = np.transpose(tile, trans) + for _ in range(squeezeCount): + tile = tile.squeeze(0) + if len(tile.shape) == 2: + tile = np.expand_dims(tile, axis=2) + return self._outputTile(tile, TILE_FORMAT_NUMPY, x, y, z, + pilImageAllowed, numpyAllowed, **kwargs)
+ + + def _validateNewTile(self, tile, mask, placement, axes): + if not isinstance(tile, np.ndarray) or axes is None: + axes = 'yxs' + tile, mode = _imageToNumpy(tile) + elif not isinstance(axes, str) and not isinstance(axes, list): + err = 'Invalid type for axes. Must be str or list[str].' + raise ValueError(err) + axes = [x.lower() for x in axes] + if axes[-1] != 's': + axes.append('s') + if mask is not None and len(axes) - 1 == len(mask.shape): + mask = mask[:, :, np.newaxis] + if 'x' not in axes or 'y' not in axes: + err = 'Invalid value for axes. Must contain "y" and "x".' + raise ValueError(err) + for k in placement: + if k not in axes: + axes[0:0] = [k] + while len(tile.shape) < len(axes): + tile = np.expand_dims(tile, axis=0) + while mask is not None and len(mask.shape) < len(axes): + mask = np.expand_dims(mask, axis=0) + + return tile, mask, placement, axes + +
+[docs] + def addTile(self, tile, x=0, y=0, mask=None, axes=None, **kwargs): + """ + Add a numpy or image tile to the image, expanding the image as needed + to accommodate it. Note that x and y can be negative. If so, the + output image (and internal memory access of the image) will act as if + the 0, 0 point is the most negative position. Cropping is applied + after this offset. + + :param tile: a numpy array, PIL Image, or a binary string + with an image. The numpy array can have 2 or 3 dimensions. + :param x: location in destination for upper-left corner. + :param y: location in destination for upper-left corner. + :param mask: a 2-d numpy array (or 3-d if the last dimension is 1). + If specified, areas where the mask is false will not be altered. + :param axes: a string or list of strings specifying the names of axes + in the same order as the tile dimensions. + :param kwargs: start locations for any additional axes. Note that + ``level`` is a reserved word and not permitted for an axis name. + """ + self._checkEditable() + store_path = str(kwargs.pop('level', 0)) + placement = { + 'x': x, + 'y': y, + **kwargs, + } + tile, mask, placement, axes = self._validateNewTile(tile, mask, placement, axes) + + with self._threadLock and self._processLock: + self._axes = {k: i for i, k in enumerate(axes)} + new_dims = { + a: max( + self._dims.get(store_path, {}).get(a, 0), + placement.get(a, 0) + tile.shape[i], + ) + for a, i in self._axes.items() + } + self._dims[store_path] = new_dims + placement_slices = tuple([ + slice(placement.get(a, 0), placement.get(a, 0) + tile.shape[i], 1) + for i, a in enumerate(axes) + ]) + + current_arrays = dict(self._zarr.arrays()) + if store_path == '0': + # if writing to base data, invalidate generated levels + for path in current_arrays: + if path != store_path: + self._zarr_store.rmdir(path) + chunking = None + if store_path not in current_arrays: + arr = np.empty(tuple(new_dims.values()), dtype=tile.dtype) + chunking = tuple([ + self._tileSize if a in ['x', 'y'] else + new_dims.get('s') if a == 's' else 1 + for a in axes + ]) + else: + arr = current_arrays[store_path] + arr.resize(*tuple(new_dims.values())) + if arr.chunks[-1] != new_dims.get('s'): + # rechunk if length of samples axis changes + chunking = tuple([ + self._tileSize if a in ['x', 'y'] else + new_dims.get('s') if a == 's' else 1 + for a in axes + ]) + + if mask is not None: + arr[placement_slices] = np.where(mask, tile, arr[placement_slices]) + else: + arr[placement_slices] = tile + if chunking: + zarr.array( + arr, + chunks=chunking, + overwrite=True, + store=self._zarr_store, + path=store_path, + ) + + # If base data changed, update large_image attributes + if store_path == '0': + self._dtype = tile.dtype + self._bandCount = new_dims.get(axes[-1]) # last axis is assumed to be bands + self.sizeX = new_dims.get('x') + self.sizeY = new_dims.get('y') + self._framecount = math.prod([ + length + for axis, length in new_dims.items() + if axis in axes[:-3] + ]) + self._cacheValue = str(uuid.uuid4()) + self._levels = None + self.levels = int(max(1, math.ceil(math.log(max( + self.sizeX / self.tileWidth, self.sizeY / self.tileHeight)) / math.log(2)) + 1))
+ + +
+[docs] + def addAssociatedImage(self, image, imageKey=None): + """ + Add an associated image to this source. + + :param image: a numpy array, PIL Image, or a binary string + with an image. The numpy array can have 2 or 3 dimensions. + """ + data, _ = _imageToNumpy(image) + with self._threadLock and self._processLock: + if imageKey is None: + # Each associated image should be in its own group + num_existing = len(self.getAssociatedImagesList()) + imageKey = f'image_{num_existing + 1}' + group = self._zarr.require_group(imageKey) + arr = zarr.array( + data, + store=self._zarr_store, + path=f'{imageKey}/image', + ) + self._associatedImages[imageKey] = (group, arr)
+ + + def _writeInternalMetadata(self): + self._checkEditable() + with self._threadLock and self._processLock: + name = str(self._tempdir.name).split('/')[-1] + arrays = dict(self._zarr.arrays()) + channel_axis = self._axes.get('c', self._axes.get('s')) + datasets = [] + axes = [] + channels = [] + rdefs = {'model': 'color' if len(self._channelColors) else 'greyscale'} + sorted_axes = [a[0] for a in sorted(self._axes.items(), key=lambda item: item[1])] + for arr_name in arrays: + level = int(arr_name) + scale = [1.0 for a in sorted_axes] + scale[self._axes.get('x')] = (self._mm_x or 0) * (2 ** level) + scale[self._axes.get('y')] = (self._mm_y or 0) * (2 ** level) + dataset_metadata = { + 'path': arr_name, + 'coordinateTransformations': [{ + 'type': 'scale', + 'scale': scale, + }], + } + datasets.append(dataset_metadata) + for a in sorted_axes: + axis_metadata = {'name': a} + if a in ['x', 'y']: + axis_metadata['type'] = 'space' + axis_metadata['unit'] = 'millimeter' + elif a in ['s', 'c']: + axis_metadata['type'] = 'channel' + elif a == 't': + rdefs['defaultT'] = 0 + elif a == 'z': + rdefs['defaultZ'] = 0 + axes.append(axis_metadata) + if channel_axis is not None and len(arrays) > 0: + base_array = list(arrays.values())[0] + base_shape = base_array.shape + for c in range(base_shape[channel_axis]): + channel_metadata = { + 'active': True, + 'coefficient': 1, + 'color': 'FFFFFF', + 'family': 'linear', + 'inverted': False, + 'label': f'Band {c + 1}', + } + slicing = tuple( + slice(None) + if k != ('c' if 'c' in self._axes else 's') + else c + for k, v in self._axes.items() + ) + channel_data = base_array[slicing] + channel_min = np.min(channel_data) + channel_max = np.max(channel_data) + channel_metadata['window'] = { + 'end': channel_max, + 'max': channel_max, + 'min': channel_min, + 'start': channel_min, + } + if len(self._channelNames) > c: + channel_metadata['label'] = self._channelNames[c] + if len(self._channelColors) > c: + channel_metadata['color'] = self._channelColors[c] + channels.append(channel_metadata) + # Guidelines from https://ngff.openmicroscopy.org/latest/ + self._zarr.attrs.update({ + 'multiscales': [{ + 'version': '0.5', + 'name': name, + 'axes': axes, + 'datasets': datasets, + 'metadata': { + 'description': self._imageDescription or '', + 'kwargs': { + 'multichannel': (channel_axis is not None), + }, + }, + }], + 'omero': { + 'id': 1, + 'version': '0.5', + 'name': name, + 'channels': channels, + 'rdefs': rdefs, + }, + 'bioformats2raw.layout': 3, + }) + + @property + def crop(self): + """ + Crop only applies to the output file, not the internal data access. + + It consists of x, y, w, h in pixels. + """ + return getattr(self, '_crop', None) + + @crop.setter + def crop(self, value): + self._checkEditable() + if value is None: + self._crop = None + return + x, y, w, h = value + x = int(x) + y = int(y) + w = int(w) + h = int(h) + if x < 0 or y < 0 or w <= 0 or h <= 0: + msg = 'Crop must have non-negative x, y and positive w, h' + raise TileSourceError(msg) + self._crop = (x, y, w, h) + + @property + def mm_x(self): + return self._mm_x + + @mm_x.setter + def mm_x(self, value): + self._checkEditable() + value = float(value) if value is not None else None + if value is not None and value <= 0: + msg = 'mm_x must be positive or None' + raise TileSourceError(msg) + self._mm_x = value + + @property + def mm_y(self): + return self._mm_y + + @mm_y.setter + def mm_y(self, value): + self._checkEditable() + value = float(value) if value is not None else None + if value is not None and value <= 0: + msg = 'mm_y must be positive or None' + raise TileSourceError(msg) + self._mm_y = value + + @property + def imageDescription(self): + return self._imageDescription + + @imageDescription.setter + def imageDescription(self, description): + self._checkEditable() + self._imageDescription = description + + @property + def channelNames(self): + return self._channelNames + + @channelNames.setter + def channelNames(self, names): + self._checkEditable() + self._channelNames = names + + @property + def channelColors(self): + return self._channelColors + + @channelColors.setter + def channelColors(self, colors): + self._checkEditable() + self._channelColors = colors + + def _generateDownsampledLevels(self, resample_method): + self._checkEditable() + current_arrays = dict(self._zarr.arrays()) + if '0' not in current_arrays: + msg = 'No root data found, cannot generate lower resolution levels.' + raise TileSourceError(msg) + if 'x' not in self._axes or 'y' not in self._axes: + msg = 'Data must have an X axis and Y axis to generate lower resolution levels.' + raise TileSourceError(msg) + + metadata = self.getMetadata() + + if ( + resample_method.value < ResampleMethod.PIL_MAX_ENUM.value and + resample_method != ResampleMethod.PIL_NEAREST + ): + tile_overlap = dict(x=4, y=4, edges=True) + else: + tile_overlap = dict(x=0, y=0) + tile_size = dict( + width=4096 + tile_overlap['x'], + height=4096 + tile_overlap['y'], + ) + sorted_axes = [a[0] for a in sorted(self._axes.items(), key=lambda item: item[1])] + for level in range(1, self.levels): + scale_factor = 2 ** level + iterator_output = dict( + maxWidth=self.sizeX // scale_factor, + maxHeight=self.sizeY // scale_factor, + ) + for frame in metadata.get('frames', [{'Index': 0}]): + frame_position = { + k.replace('Index', '').lower(): v + for k, v in frame.items() + if k.replace('Index', '').lower() in self._axes + } + for tile in self.tileIterator( + tile_size=tile_size, + tile_overlap=tile_overlap, + frame=frame['Index'], + output=iterator_output, + resample=False, # TODO: incorporate resampling in core + ): + new_tile = downsampleTileHalfRes(tile['tile'], resample_method) + overlap = {k: int(v / 2) for k, v in tile['tile_overlap'].items()} + new_tile = new_tile[ + slice(overlap['top'], new_tile.shape[0] - overlap['bottom']), + slice(overlap['left'], new_tile.shape[1] - overlap['right']), + ] + + x = int(tile['x'] / 2 + overlap['left']) + y = int(tile['y'] / 2 + overlap['top']) + + self.addTile( + new_tile, + x=x, + y=y, + **frame_position, + axes=sorted_axes, + level=level, + ) + self._validateZarr() # refresh self._levels before continuing + +
+[docs] + def write( + self, + path, + lossy=True, + alpha=True, + overwriteAllowed=True, + resample=None, + **converterParams, + ): + """ + Output the current image to a file. + + :param path: output path. + :param lossy: if false, emit a lossless file. + :param alpha: True if an alpha channel is allowed. + :param overwriteAllowed: if False, raise an exception if the output + path exists. + :param resample: one of the ``ResampleMethod`` enum values. Defaults + to ``NP_NEAREST`` for lossless and non-uint8 data and to + ``PIL_LANCZOS`` for lossy uint8 data. + :param converterParams: options to pass to the large_image_converter if + the output is not a zarr variant. + """ + if os.path.exists(path): + if overwriteAllowed: + if os.path.isdir(path): + shutil.rmtree(path) + else: + os.remove(path) + else: + raise TileSourceError('Output path exists (%s).' % str(path)) + + suffix = Path(path).suffix + source = self + + if self.crop: + left, top, width, height = self.crop + source = new() + source._zarr.attrs.update(self._zarr.attrs) + for frame in self.getMetadata().get('frames', [{'Index': 0}]): + frame_position = { + k.replace('Index', '').lower(): v + for k, v in frame.items() + if k.replace('Index', '').lower() in self._axes + } + for tile in self.tileIterator( + frame=frame['Index'], + region=dict(top=top, left=left, width=width, height=height), + resample=False, + ): + source.addTile( + tile['tile'], + x=tile['x'] - left, + y=tile['y'] - top, + axes=list(self._axes.keys()), + **frame_position, + ) + + source._validateZarr() + + if suffix in ['.zarr', '.db', '.sqlite', '.zip']: + if resample is None: + resample = ( + ResampleMethod.PIL_LANCZOS + if lossy and source.dtype == np.uint8 + else ResampleMethod.NP_NEAREST + ) + source._generateDownsampledLevels(resample) + source._writeInternalMetadata() # rewrite with new level datasets + + if suffix == '.zarr': + shutil.copytree(str(source._tempdir), path) + elif suffix in ['.db', '.sqlite']: + sqlite_store = zarr.SQLiteStore(path) + zarr.copy_store(source._zarr_store, sqlite_store, if_exists='replace') + sqlite_store.close() + elif suffix == '.zip': + zip_store = zarr.ZipStore(path) + zarr.copy_store(source._zarr_store, zip_store, if_exists='replace') + zip_store.close() + + else: + from large_image_converter import convert + + attrs_path = source._tempdir / '.zattrs' + params = {} + if lossy and self.dtype == np.uint8: + params['compression'] = 'jpeg' + params.update(converterParams) + convert(str(attrs_path), path, overwrite=overwriteAllowed, **params)
+
+ + + +
+[docs] +def open(*args, **kwargs): + """ + Create an instance of the module class. + """ + return ZarrFileTileSource(*args, **kwargs)
+ + + +
+[docs] +def canRead(*args, **kwargs): + """ + Check if an input can be read by the module class. + """ + return ZarrFileTileSource.canRead(*args, **kwargs)
+ + + +
+[docs] +def new(*args, **kwargs): + """ + Create a new image, collecting the results from patches of numpy arrays or + smaller images. + """ + return ZarrFileTileSource(NEW_IMAGE_PATH_FLAG + str(uuid.uuid4()), *args, **kwargs)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_source_zarr/girder_source.html b/_modules/large_image_source_zarr/girder_source.html new file mode 100644 index 000000000..91c21cc13 --- /dev/null +++ b/_modules/large_image_source_zarr/girder_source.html @@ -0,0 +1,136 @@ + + + + + + large_image_source_zarr.girder_source — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_source_zarr.girder_source

+from girder_large_image.girder_tilesource import GirderTileSource
+
+from . import ZarrFileTileSource
+
+
+
+[docs] +class ZarrGirderTileSource(ZarrFileTileSource, GirderTileSource): + """ + Provides tile access to Girder items with files that OME Zarr can read. + """ + + cacheName = 'tilesource' + name = 'zarr' + + _mayHaveAdjacentFiles = True
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_tasks.html b/_modules/large_image_tasks.html new file mode 100644 index 000000000..a7a5b2e1a --- /dev/null +++ b/_modules/large_image_tasks.html @@ -0,0 +1,150 @@ + + + + + + large_image_tasks — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_tasks

+"""Top-level package for Large Image Tasks."""
+
+__author__ = """Kitware Inc"""
+__email__ = 'kitware@kitware.com'
+
+
+from importlib.metadata import PackageNotFoundError
+from importlib.metadata import version as _importlib_version
+
+from girder_worker import GirderWorkerPluginABC
+
+try:
+    __version__ = _importlib_version(__name__)
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+
+
+[docs] +class LargeImageTasks(GirderWorkerPluginABC): + def __init__(self, app, *args, **kwargs): + self.app = app + +
+[docs] + def task_imports(self): + # Return a list of python importable paths to the + # plugin's path directory + return ['large_image_tasks.tasks']
+
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/large_image_tasks/tasks.html b/_modules/large_image_tasks/tasks.html new file mode 100644 index 000000000..f6c350937 --- /dev/null +++ b/_modules/large_image_tasks/tasks.html @@ -0,0 +1,357 @@ + + + + + + large_image_tasks.tasks — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for large_image_tasks.tasks

+import inspect
+import logging
+import os
+import shutil
+import sys
+import time
+
+from girder_worker.app import app
+from girder_worker.utils import girder_job
+
+
+@girder_job(title='Create a pyramidal tiff using vips', type='large_image_tiff')
+@app.task(bind=True)
+def create_tiff(self, inputFile, outputName=None, outputDir=None, quality=90,
+                tileSize=256, **kwargs):
+    """
+    Take a source input file, readable by vips, and output a pyramidal tiff
+    file.
+
+    :param inputFile: the path to the input file or base file of a set.
+    :param outputName: the name of the output file.  If None, the name is
+        based on the input name and current date and time.  May be a full path.
+    :param outputDir: the location to store the output.  If unspecified, the
+        inputFile's directory is used.  If the outputName is a fully qualified
+        path, this is ignored.
+    :param quality: a jpeg quality passed to vips.  0 is small, 100 is high
+        quality.  90 or above is recommended.
+    :param tileSize: the horizontal and vertical tile size.
+    Optional parameters that can be specified in kwargs:
+    :param compression: one of 'jpeg', 'deflate' (zip), 'lzw', 'packbits', or
+        'zstd'.
+    :param level: compression level for zstd, 1-22 (default is 10).
+    :param predictor: one of 'none', 'horizontal', or 'float' used for lzw and
+        deflate.
+    :param inputName: if no output name is specified, and this is specified,
+        this is used as the basis of the output name instead of extracting the
+        name from the inputFile path.
+    :returns: output path.
+    """
+    import large_image_converter
+
+    logger = logging.getLogger('large-image-converter')
+    if not len(logger.handlers):
+        logger.addHandler(logging.StreamHandler(sys.stdout))
+    if not logger.level:
+        logger.setLevel(logging.INFO)
+
+    if '_concurrency' not in kwargs:
+        kwargs['_concurrency'] = -2
+    inputPath = os.path.abspath(os.path.expanduser(inputFile))
+    geospatial = large_image_converter.is_geospatial(inputPath)
+    inputName = kwargs.get('inputName', os.path.basename(inputPath))
+    suffix = large_image_converter.format_hook('adjust_params', geospatial, kwargs, **kwargs)
+    suffix = suffix or ('.tiff' if not geospatial else '.geo.tiff')
+    if not outputName:
+        outputName = os.path.splitext(inputName)[0] + suffix
+        if outputName.endswith('.geo' + suffix):
+            outputName = outputName[:len(outputName) - len(suffix) - 4] + suffix
+        if outputName == inputName:
+            outputName = (os.path.splitext(inputName)[0] + '.' +
+                          time.strftime('%Y%m%d-%H%M%S') + suffix)
+    renameOutput = outputName
+    if not outputName.endswith(suffix):
+        outputName += suffix
+    if not outputDir:
+        outputDir = os.path.dirname(inputPath)
+    outputPath = os.path.join(outputDir, outputName)
+    large_image_converter.convert(
+        inputPath, outputPath, quality=quality, tileSize=tileSize, **kwargs)
+    if not os.path.exists(outputPath):
+        msg = 'Conversion command failed to produce output'
+        raise Exception(msg)
+    if renameOutput != outputName:
+        renamePath = os.path.join(outputDir, renameOutput)
+        shutil.move(outputPath, renamePath)
+        outputPath = renamePath
+    logger.info('Created a file of size %d', os.path.getsize(outputPath))
+    return outputPath
+
+
+
+[docs] +class JobLogger(logging.Handler): + def __init__(self, level=logging.NOTSET, job=None, *args, **kwargs): + self._job = job + super().__init__(level=level, **kwargs) + +
+[docs] + def emit(self, record): + from girder_jobs.models.job import Job + + self._job = Job().updateJob(self._job, log=self.format(record).rstrip() + '\n')
+
+ + + +
+[docs] +def convert_image_job(job): + import tempfile + + from girder_jobs.constants import JobStatus + from girder_jobs.models.job import Job + + from girder.constants import AccessType + from girder.models.file import File + from girder.models.folder import Folder + from girder.models.item import Item + from girder.models.upload import Upload + from girder.models.user import User + + kwargs = job['kwargs'] + toFolder = kwargs.pop('toFolder', True) + item = Item().load(kwargs.pop('itemId'), force=True) + fileObj = File().load(kwargs.pop('fileId'), force=True) + userId = kwargs.pop('userId', None) + user = User().load(userId, force=True) if userId else None + if toFolder: + parentType = 'folder' + parent = Folder().load(kwargs.pop('folderId', item['folderId']), + user=user, level=AccessType.WRITE) + else: + parentType = 'item' + parent = item + name = kwargs.pop('name', None) + + job = Job().updateJob( + job, log='Started large image conversion\n', + status=JobStatus.RUNNING) + logger = logging.getLogger('large-image-converter') + handler = JobLogger(job=job) + logger.addHandler(handler) + # We could increase the default logging level here + # logger.setLevel(logging.DEBUG) + try: + inputPath = None + if not fileObj.get('imported'): + try: + inputPath = File().getGirderMountFilePath( + fileObj, + **({'preferFlat': True} if 'preferFlat' in inspect.signature( + File.getGirderMountFilePath).parameters else {})) + except Exception: + pass + inputPath = inputPath or File().getLocalFilePath(fileObj) + with tempfile.TemporaryDirectory() as tempdir: + dest = create_tiff( + inputFile=inputPath, + inputName=fileObj['name'], + outputDir=tempdir, + **kwargs, + ) + job = Job().updateJob(job, log='Storing result\n') + with open(dest, 'rb') as fobj: + fileObj = Upload().uploadFromFile( + fobj, + size=os.path.getsize(dest), + name=name or os.path.basename(dest), + parentType=parentType, + parent=parent, + user=user, + ) + job = Job().load(job['_id'], force=True) + job.setdefault('results', {}) + job['results'].setdefault('file', []) + job['results']['file'].append(fileObj['_id']) + job = Job().save(job) + except Exception as exc: + status = JobStatus.ERROR + logger.exception('Failed in large image conversion') + job = Job().updateJob( + job, log='Failed in large image conversion (%s)\n' % exc, status=status) + else: + status = JobStatus.SUCCESS + job = Job().updateJob( + job, log='Finished large image conversion\n', status=status) + finally: + logger.removeHandler(handler)
+ + + +
+[docs] +def cache_tile_frames_job(job): + from girder_jobs.constants import JobStatus + from girder_jobs.models.job import Job + from girder_large_image.models.image_item import ImageItem + + from girder import logger + + kwargs = job['kwargs'] + item = ImageItem().load(kwargs.pop('itemId'), force=True) + job = Job().updateJob( + job, log='Started caching tile frames\n', + status=JobStatus.RUNNING) + try: + for entry in kwargs.get('tileFramesList'): + job = Job().load(job['_id'], force=True) + if job['status'] == JobStatus.CANCELED: + return + job = Job().updateJob(job, log='Caching %r\n' % entry) + ImageItem().tileFrames(item, checkAndCreate=True, **entry) + job = Job().updateJob(job, log='Finished caching tile frames\n', status=JobStatus.SUCCESS) + except Exception as exc: + logger.exception('Failed caching tile frames') + job = Job().updateJob( + job, log='Failed caching tile frames (%s)\n' % exc, status=JobStatus.ERROR)
+ + + +
+[docs] +def cache_histograms_job(job): + from girder_jobs.constants import JobStatus + from girder_jobs.models.job import Job + from girder_large_image.models.image_item import ImageItem + + from girder import logger + + kwargs = job['kwargs'] + item = ImageItem().load(kwargs.pop('itemId'), force=True) + job = Job().updateJob( + job, log='Started caching histograms\n', + status=JobStatus.RUNNING) + try: + for entry in kwargs.get('histogramList'): + job = Job().load(job['_id'], force=True) + if job['status'] == JobStatus.CANCELED: + return + job = Job().updateJob(job, log='Caching %r\n' % entry) + ImageItem().histogram(item, checkAndCreate=True, **entry) + job = Job().updateJob(job, log='Finished caching histograms\n', status=JobStatus.SUCCESS) + except Exception as exc: + logger.exception('Failed caching histograms') + job = Job().updateJob( + job, log='Failed caching histograms (%s)\n' % exc, status=JobStatus.ERROR)
+ +
+ +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_sources/_build/girder_large_image/girder_large_image.models.rst.txt b/_sources/_build/girder_large_image/girder_large_image.models.rst.txt new file mode 100644 index 000000000..0a4cdb284 --- /dev/null +++ b/_sources/_build/girder_large_image/girder_large_image.models.rst.txt @@ -0,0 +1,21 @@ +girder\_large\_image.models package +=================================== + +Submodules +---------- + +girder\_large\_image.models.image\_item module +---------------------------------------------- + +.. automodule:: girder_large_image.models.image_item + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: girder_large_image.models + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/girder_large_image/girder_large_image.rest.rst.txt b/_sources/_build/girder_large_image/girder_large_image.rest.rst.txt new file mode 100644 index 000000000..dc38c78c6 --- /dev/null +++ b/_sources/_build/girder_large_image/girder_large_image.rest.rst.txt @@ -0,0 +1,37 @@ +girder\_large\_image.rest package +================================= + +Submodules +---------- + +girder\_large\_image.rest.item\_meta module +------------------------------------------- + +.. automodule:: girder_large_image.rest.item_meta + :members: + :undoc-members: + :show-inheritance: + +girder\_large\_image.rest.large\_image\_resource module +------------------------------------------------------- + +.. automodule:: girder_large_image.rest.large_image_resource + :members: + :undoc-members: + :show-inheritance: + +girder\_large\_image.rest.tiles module +-------------------------------------- + +.. automodule:: girder_large_image.rest.tiles + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: girder_large_image.rest + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/girder_large_image/girder_large_image.rst.txt b/_sources/_build/girder_large_image/girder_large_image.rst.txt new file mode 100644 index 000000000..1b5559837 --- /dev/null +++ b/_sources/_build/girder_large_image/girder_large_image.rst.txt @@ -0,0 +1,46 @@ +girder\_large\_image package +============================ + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + girder_large_image.models + girder_large_image.rest + +Submodules +---------- + +girder\_large\_image.constants module +------------------------------------- + +.. automodule:: girder_large_image.constants + :members: + :undoc-members: + :show-inheritance: + +girder\_large\_image.girder\_tilesource module +---------------------------------------------- + +.. automodule:: girder_large_image.girder_tilesource + :members: + :undoc-members: + :show-inheritance: + +girder\_large\_image.loadmodelcache module +------------------------------------------ + +.. automodule:: girder_large_image.loadmodelcache + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: girder_large_image + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/girder_large_image/modules.rst.txt b/_sources/_build/girder_large_image/modules.rst.txt new file mode 100644 index 000000000..f7a55b0f2 --- /dev/null +++ b/_sources/_build/girder_large_image/modules.rst.txt @@ -0,0 +1,7 @@ +girder_large_image +================== + +.. toctree:: + :maxdepth: 4 + + girder_large_image diff --git a/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.models.rst.txt b/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.models.rst.txt new file mode 100644 index 000000000..8571bfbe1 --- /dev/null +++ b/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.models.rst.txt @@ -0,0 +1,29 @@ +girder\_large\_image\_annotation.models package +=============================================== + +Submodules +---------- + +girder\_large\_image\_annotation.models.annotation module +--------------------------------------------------------- + +.. automodule:: girder_large_image_annotation.models.annotation + :members: + :undoc-members: + :show-inheritance: + +girder\_large\_image\_annotation.models.annotationelement module +---------------------------------------------------------------- + +.. automodule:: girder_large_image_annotation.models.annotationelement + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: girder_large_image_annotation.models + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.rest.rst.txt b/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.rest.rst.txt new file mode 100644 index 000000000..05889a570 --- /dev/null +++ b/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.rest.rst.txt @@ -0,0 +1,21 @@ +girder\_large\_image\_annotation.rest package +============================================= + +Submodules +---------- + +girder\_large\_image\_annotation.rest.annotation module +------------------------------------------------------- + +.. automodule:: girder_large_image_annotation.rest.annotation + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: girder_large_image_annotation.rest + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.rst.txt b/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.rst.txt new file mode 100644 index 000000000..fa7274eec --- /dev/null +++ b/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.rst.txt @@ -0,0 +1,39 @@ +girder\_large\_image\_annotation package +======================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + girder_large_image_annotation.models + girder_large_image_annotation.rest + girder_large_image_annotation.utils + +Submodules +---------- + +girder\_large\_image\_annotation.constants module +------------------------------------------------- + +.. automodule:: girder_large_image_annotation.constants + :members: + :undoc-members: + :show-inheritance: + +girder\_large\_image\_annotation.handlers module +------------------------------------------------ + +.. automodule:: girder_large_image_annotation.handlers + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: girder_large_image_annotation + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.utils.rst.txt b/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.utils.rst.txt new file mode 100644 index 000000000..6a05d840c --- /dev/null +++ b/_sources/_build/girder_large_image_annotation/girder_large_image_annotation.utils.rst.txt @@ -0,0 +1,10 @@ +girder\_large\_image\_annotation.utils package +============================================== + +Module contents +--------------- + +.. automodule:: girder_large_image_annotation.utils + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/girder_large_image_annotation/modules.rst.txt b/_sources/_build/girder_large_image_annotation/modules.rst.txt new file mode 100644 index 000000000..19ff8a826 --- /dev/null +++ b/_sources/_build/girder_large_image_annotation/modules.rst.txt @@ -0,0 +1,7 @@ +girder_large_image_annotation +============================= + +.. toctree:: + :maxdepth: 4 + + girder_large_image_annotation diff --git a/_sources/_build/large_image/large_image.cache_util.rst.txt b/_sources/_build/large_image/large_image.cache_util.rst.txt new file mode 100644 index 000000000..64c5b1684 --- /dev/null +++ b/_sources/_build/large_image/large_image.cache_util.rst.txt @@ -0,0 +1,53 @@ +large\_image.cache\_util package +================================ + +Submodules +---------- + +large\_image.cache\_util.base module +------------------------------------ + +.. automodule:: large_image.cache_util.base + :members: + :undoc-members: + :show-inheritance: + +large\_image.cache\_util.cache module +------------------------------------- + +.. automodule:: large_image.cache_util.cache + :members: + :undoc-members: + :show-inheritance: + +large\_image.cache\_util.cachefactory module +-------------------------------------------- + +.. automodule:: large_image.cache_util.cachefactory + :members: + :undoc-members: + :show-inheritance: + +large\_image.cache\_util.memcache module +---------------------------------------- + +.. automodule:: large_image.cache_util.memcache + :members: + :undoc-members: + :show-inheritance: + +large\_image.cache\_util.rediscache module +------------------------------------------ + +.. automodule:: large_image.cache_util.rediscache + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image.cache_util + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image/large_image.rst.txt b/_sources/_build/large_image/large_image.rst.txt new file mode 100644 index 000000000..23f21ae0d --- /dev/null +++ b/_sources/_build/large_image/large_image.rst.txt @@ -0,0 +1,46 @@ +large\_image package +==================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + large_image.cache_util + large_image.tilesource + +Submodules +---------- + +large\_image.config module +-------------------------- + +.. automodule:: large_image.config + :members: + :undoc-members: + :show-inheritance: + +large\_image.constants module +----------------------------- + +.. automodule:: large_image.constants + :members: + :undoc-members: + :show-inheritance: + +large\_image.exceptions module +------------------------------ + +.. automodule:: large_image.exceptions + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image/large_image.tilesource.rst.txt b/_sources/_build/large_image/large_image.tilesource.rst.txt new file mode 100644 index 000000000..1c87acfd1 --- /dev/null +++ b/_sources/_build/large_image/large_image.tilesource.rst.txt @@ -0,0 +1,77 @@ +large\_image.tilesource package +=============================== + +Submodules +---------- + +large\_image.tilesource.base module +----------------------------------- + +.. automodule:: large_image.tilesource.base + :members: + :undoc-members: + :show-inheritance: + +large\_image.tilesource.geo module +---------------------------------- + +.. automodule:: large_image.tilesource.geo + :members: + :undoc-members: + :show-inheritance: + +large\_image.tilesource.jupyter module +-------------------------------------- + +.. automodule:: large_image.tilesource.jupyter + :members: + :undoc-members: + :show-inheritance: + +large\_image.tilesource.resample module +--------------------------------------- + +.. automodule:: large_image.tilesource.resample + :members: + :undoc-members: + :show-inheritance: + +large\_image.tilesource.stylefuncs module +----------------------------------------- + +.. automodule:: large_image.tilesource.stylefuncs + :members: + :undoc-members: + :show-inheritance: + +large\_image.tilesource.tiledict module +--------------------------------------- + +.. automodule:: large_image.tilesource.tiledict + :members: + :undoc-members: + :show-inheritance: + +large\_image.tilesource.tileiterator module +------------------------------------------- + +.. automodule:: large_image.tilesource.tileiterator + :members: + :undoc-members: + :show-inheritance: + +large\_image.tilesource.utilities module +---------------------------------------- + +.. automodule:: large_image.tilesource.utilities + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image.tilesource + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image/modules.rst.txt b/_sources/_build/large_image/modules.rst.txt new file mode 100644 index 000000000..81050fb80 --- /dev/null +++ b/_sources/_build/large_image/modules.rst.txt @@ -0,0 +1,7 @@ +large_image +=========== + +.. toctree:: + :maxdepth: 4 + + large_image diff --git a/_sources/_build/large_image_converter/large_image_converter.rst.txt b/_sources/_build/large_image_converter/large_image_converter.rst.txt new file mode 100644 index 000000000..c2dd37c5e --- /dev/null +++ b/_sources/_build/large_image_converter/large_image_converter.rst.txt @@ -0,0 +1,21 @@ +large\_image\_converter package +=============================== + +Submodules +---------- + +large\_image\_converter.format\_aperio module +--------------------------------------------- + +.. automodule:: large_image_converter.format_aperio + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_converter + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_converter/modules.rst.txt b/_sources/_build/large_image_converter/modules.rst.txt new file mode 100644 index 000000000..7ebaa6053 --- /dev/null +++ b/_sources/_build/large_image_converter/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_converter +===================== + +.. toctree:: + :maxdepth: 4 + + large_image_converter diff --git a/_sources/_build/large_image_source_bioformats/large_image_source_bioformats.rst.txt b/_sources/_build/large_image_source_bioformats/large_image_source_bioformats.rst.txt new file mode 100644 index 000000000..c82bbbbb0 --- /dev/null +++ b/_sources/_build/large_image_source_bioformats/large_image_source_bioformats.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_bioformats package +======================================== + +Submodules +---------- + +large\_image\_source\_bioformats.girder\_source module +------------------------------------------------------ + +.. automodule:: large_image_source_bioformats.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_bioformats + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_bioformats/modules.rst.txt b/_sources/_build/large_image_source_bioformats/modules.rst.txt new file mode 100644 index 000000000..c697c3a9e --- /dev/null +++ b/_sources/_build/large_image_source_bioformats/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_bioformats +============================= + +.. toctree:: + :maxdepth: 4 + + large_image_source_bioformats diff --git a/_sources/_build/large_image_source_deepzoom/large_image_source_deepzoom.rst.txt b/_sources/_build/large_image_source_deepzoom/large_image_source_deepzoom.rst.txt new file mode 100644 index 000000000..e6cfa07ac --- /dev/null +++ b/_sources/_build/large_image_source_deepzoom/large_image_source_deepzoom.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_deepzoom package +====================================== + +Submodules +---------- + +large\_image\_source\_deepzoom.girder\_source module +---------------------------------------------------- + +.. automodule:: large_image_source_deepzoom.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_deepzoom + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_deepzoom/modules.rst.txt b/_sources/_build/large_image_source_deepzoom/modules.rst.txt new file mode 100644 index 000000000..79954c2e0 --- /dev/null +++ b/_sources/_build/large_image_source_deepzoom/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_deepzoom +=========================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_deepzoom diff --git a/_sources/_build/large_image_source_dicom/large_image_source_dicom.assetstore.rst.txt b/_sources/_build/large_image_source_dicom/large_image_source_dicom.assetstore.rst.txt new file mode 100644 index 000000000..cc3279af9 --- /dev/null +++ b/_sources/_build/large_image_source_dicom/large_image_source_dicom.assetstore.rst.txt @@ -0,0 +1,29 @@ +large\_image\_source\_dicom.assetstore package +============================================== + +Submodules +---------- + +large\_image\_source\_dicom.assetstore.dicomweb\_assetstore\_adapter module +--------------------------------------------------------------------------- + +.. automodule:: large_image_source_dicom.assetstore.dicomweb_assetstore_adapter + :members: + :undoc-members: + :show-inheritance: + +large\_image\_source\_dicom.assetstore.rest module +-------------------------------------------------- + +.. automodule:: large_image_source_dicom.assetstore.rest + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_dicom.assetstore + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_dicom/large_image_source_dicom.rst.txt b/_sources/_build/large_image_source_dicom/large_image_source_dicom.rst.txt new file mode 100644 index 000000000..54a672069 --- /dev/null +++ b/_sources/_build/large_image_source_dicom/large_image_source_dicom.rst.txt @@ -0,0 +1,61 @@ +large\_image\_source\_dicom package +=================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + large_image_source_dicom.assetstore + +Submodules +---------- + +large\_image\_source\_dicom.dicom\_metadata module +-------------------------------------------------- + +.. automodule:: large_image_source_dicom.dicom_metadata + :members: + :undoc-members: + :show-inheritance: + +large\_image\_source\_dicom.dicom\_tags module +---------------------------------------------- + +.. automodule:: large_image_source_dicom.dicom_tags + :members: + :undoc-members: + :show-inheritance: + +large\_image\_source\_dicom.dicomweb\_utils module +-------------------------------------------------- + +.. automodule:: large_image_source_dicom.dicomweb_utils + :members: + :undoc-members: + :show-inheritance: + +large\_image\_source\_dicom.girder\_plugin module +------------------------------------------------- + +.. automodule:: large_image_source_dicom.girder_plugin + :members: + :undoc-members: + :show-inheritance: + +large\_image\_source\_dicom.girder\_source module +------------------------------------------------- + +.. automodule:: large_image_source_dicom.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_dicom + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_dicom/modules.rst.txt b/_sources/_build/large_image_source_dicom/modules.rst.txt new file mode 100644 index 000000000..47d73065d --- /dev/null +++ b/_sources/_build/large_image_source_dicom/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_dicom +======================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_dicom diff --git a/_sources/_build/large_image_source_dummy/large_image_source_dummy.rst.txt b/_sources/_build/large_image_source_dummy/large_image_source_dummy.rst.txt new file mode 100644 index 000000000..0855bb76c --- /dev/null +++ b/_sources/_build/large_image_source_dummy/large_image_source_dummy.rst.txt @@ -0,0 +1,10 @@ +large\_image\_source\_dummy package +=================================== + +Module contents +--------------- + +.. automodule:: large_image_source_dummy + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_dummy/modules.rst.txt b/_sources/_build/large_image_source_dummy/modules.rst.txt new file mode 100644 index 000000000..bf60579b6 --- /dev/null +++ b/_sources/_build/large_image_source_dummy/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_dummy +======================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_dummy diff --git a/_sources/_build/large_image_source_gdal/large_image_source_gdal.rst.txt b/_sources/_build/large_image_source_gdal/large_image_source_gdal.rst.txt new file mode 100644 index 000000000..5a92459ea --- /dev/null +++ b/_sources/_build/large_image_source_gdal/large_image_source_gdal.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_gdal package +================================== + +Submodules +---------- + +large\_image\_source\_gdal.girder\_source module +------------------------------------------------ + +.. automodule:: large_image_source_gdal.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_gdal + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_gdal/modules.rst.txt b/_sources/_build/large_image_source_gdal/modules.rst.txt new file mode 100644 index 000000000..30d3df38c --- /dev/null +++ b/_sources/_build/large_image_source_gdal/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_gdal +======================= + +.. toctree:: + :maxdepth: 4 + + large_image_source_gdal diff --git a/_sources/_build/large_image_source_mapnik/large_image_source_mapnik.rst.txt b/_sources/_build/large_image_source_mapnik/large_image_source_mapnik.rst.txt new file mode 100644 index 000000000..27ffecd9b --- /dev/null +++ b/_sources/_build/large_image_source_mapnik/large_image_source_mapnik.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_mapnik package +==================================== + +Submodules +---------- + +large\_image\_source\_mapnik.girder\_source module +-------------------------------------------------- + +.. automodule:: large_image_source_mapnik.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_mapnik + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_mapnik/modules.rst.txt b/_sources/_build/large_image_source_mapnik/modules.rst.txt new file mode 100644 index 000000000..91c08da24 --- /dev/null +++ b/_sources/_build/large_image_source_mapnik/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_mapnik +========================= + +.. toctree:: + :maxdepth: 4 + + large_image_source_mapnik diff --git a/_sources/_build/large_image_source_multi/large_image_source_multi.rst.txt b/_sources/_build/large_image_source_multi/large_image_source_multi.rst.txt new file mode 100644 index 000000000..fffd3e41e --- /dev/null +++ b/_sources/_build/large_image_source_multi/large_image_source_multi.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_multi package +=================================== + +Submodules +---------- + +large\_image\_source\_multi.girder\_source module +------------------------------------------------- + +.. automodule:: large_image_source_multi.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_multi + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_multi/modules.rst.txt b/_sources/_build/large_image_source_multi/modules.rst.txt new file mode 100644 index 000000000..832800e8a --- /dev/null +++ b/_sources/_build/large_image_source_multi/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_multi +======================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_multi diff --git a/_sources/_build/large_image_source_nd2/large_image_source_nd2.rst.txt b/_sources/_build/large_image_source_nd2/large_image_source_nd2.rst.txt new file mode 100644 index 000000000..f0cf5f678 --- /dev/null +++ b/_sources/_build/large_image_source_nd2/large_image_source_nd2.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_nd2 package +================================= + +Submodules +---------- + +large\_image\_source\_nd2.girder\_source module +----------------------------------------------- + +.. automodule:: large_image_source_nd2.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_nd2 + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_nd2/modules.rst.txt b/_sources/_build/large_image_source_nd2/modules.rst.txt new file mode 100644 index 000000000..67f164d3a --- /dev/null +++ b/_sources/_build/large_image_source_nd2/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_nd2 +====================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_nd2 diff --git a/_sources/_build/large_image_source_ometiff/large_image_source_ometiff.rst.txt b/_sources/_build/large_image_source_ometiff/large_image_source_ometiff.rst.txt new file mode 100644 index 000000000..b8bfae210 --- /dev/null +++ b/_sources/_build/large_image_source_ometiff/large_image_source_ometiff.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_ometiff package +===================================== + +Submodules +---------- + +large\_image\_source\_ometiff.girder\_source module +--------------------------------------------------- + +.. automodule:: large_image_source_ometiff.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_ometiff + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_ometiff/modules.rst.txt b/_sources/_build/large_image_source_ometiff/modules.rst.txt new file mode 100644 index 000000000..9ee6a253e --- /dev/null +++ b/_sources/_build/large_image_source_ometiff/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_ometiff +========================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_ometiff diff --git a/_sources/_build/large_image_source_openjpeg/large_image_source_openjpeg.rst.txt b/_sources/_build/large_image_source_openjpeg/large_image_source_openjpeg.rst.txt new file mode 100644 index 000000000..1a3285d26 --- /dev/null +++ b/_sources/_build/large_image_source_openjpeg/large_image_source_openjpeg.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_openjpeg package +====================================== + +Submodules +---------- + +large\_image\_source\_openjpeg.girder\_source module +---------------------------------------------------- + +.. automodule:: large_image_source_openjpeg.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_openjpeg + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_openjpeg/modules.rst.txt b/_sources/_build/large_image_source_openjpeg/modules.rst.txt new file mode 100644 index 000000000..2851daffb --- /dev/null +++ b/_sources/_build/large_image_source_openjpeg/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_openjpeg +=========================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_openjpeg diff --git a/_sources/_build/large_image_source_openslide/large_image_source_openslide.rst.txt b/_sources/_build/large_image_source_openslide/large_image_source_openslide.rst.txt new file mode 100644 index 000000000..dbdc14c5c --- /dev/null +++ b/_sources/_build/large_image_source_openslide/large_image_source_openslide.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_openslide package +======================================= + +Submodules +---------- + +large\_image\_source\_openslide.girder\_source module +----------------------------------------------------- + +.. automodule:: large_image_source_openslide.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_openslide + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_openslide/modules.rst.txt b/_sources/_build/large_image_source_openslide/modules.rst.txt new file mode 100644 index 000000000..d43ed6ba6 --- /dev/null +++ b/_sources/_build/large_image_source_openslide/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_openslide +============================ + +.. toctree:: + :maxdepth: 4 + + large_image_source_openslide diff --git a/_sources/_build/large_image_source_pil/large_image_source_pil.rst.txt b/_sources/_build/large_image_source_pil/large_image_source_pil.rst.txt new file mode 100644 index 000000000..3ad3d7329 --- /dev/null +++ b/_sources/_build/large_image_source_pil/large_image_source_pil.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_pil package +================================= + +Submodules +---------- + +large\_image\_source\_pil.girder\_source module +----------------------------------------------- + +.. automodule:: large_image_source_pil.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_pil + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_pil/modules.rst.txt b/_sources/_build/large_image_source_pil/modules.rst.txt new file mode 100644 index 000000000..8dd841c77 --- /dev/null +++ b/_sources/_build/large_image_source_pil/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_pil +====================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_pil diff --git a/_sources/_build/large_image_source_rasterio/large_image_source_rasterio.rst.txt b/_sources/_build/large_image_source_rasterio/large_image_source_rasterio.rst.txt new file mode 100644 index 000000000..dd3693bf0 --- /dev/null +++ b/_sources/_build/large_image_source_rasterio/large_image_source_rasterio.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_rasterio package +====================================== + +Submodules +---------- + +large\_image\_source\_rasterio.girder\_source module +---------------------------------------------------- + +.. automodule:: large_image_source_rasterio.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_rasterio + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_rasterio/modules.rst.txt b/_sources/_build/large_image_source_rasterio/modules.rst.txt new file mode 100644 index 000000000..265916edc --- /dev/null +++ b/_sources/_build/large_image_source_rasterio/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_rasterio +=========================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_rasterio diff --git a/_sources/_build/large_image_source_test/large_image_source_test.rst.txt b/_sources/_build/large_image_source_test/large_image_source_test.rst.txt new file mode 100644 index 000000000..66137b332 --- /dev/null +++ b/_sources/_build/large_image_source_test/large_image_source_test.rst.txt @@ -0,0 +1,10 @@ +large\_image\_source\_test package +================================== + +Module contents +--------------- + +.. automodule:: large_image_source_test + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_test/modules.rst.txt b/_sources/_build/large_image_source_test/modules.rst.txt new file mode 100644 index 000000000..5fedaff59 --- /dev/null +++ b/_sources/_build/large_image_source_test/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_test +======================= + +.. toctree:: + :maxdepth: 4 + + large_image_source_test diff --git a/_sources/_build/large_image_source_tiff/large_image_source_tiff.rst.txt b/_sources/_build/large_image_source_tiff/large_image_source_tiff.rst.txt new file mode 100644 index 000000000..ece4ebf0e --- /dev/null +++ b/_sources/_build/large_image_source_tiff/large_image_source_tiff.rst.txt @@ -0,0 +1,37 @@ +large\_image\_source\_tiff package +================================== + +Submodules +---------- + +large\_image\_source\_tiff.exceptions module +-------------------------------------------- + +.. automodule:: large_image_source_tiff.exceptions + :members: + :undoc-members: + :show-inheritance: + +large\_image\_source\_tiff.girder\_source module +------------------------------------------------ + +.. automodule:: large_image_source_tiff.girder_source + :members: + :undoc-members: + :show-inheritance: + +large\_image\_source\_tiff.tiff\_reader module +---------------------------------------------- + +.. automodule:: large_image_source_tiff.tiff_reader + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_tiff + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_tiff/modules.rst.txt b/_sources/_build/large_image_source_tiff/modules.rst.txt new file mode 100644 index 000000000..b681cbd2e --- /dev/null +++ b/_sources/_build/large_image_source_tiff/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_tiff +======================= + +.. toctree:: + :maxdepth: 4 + + large_image_source_tiff diff --git a/_sources/_build/large_image_source_tifffile/large_image_source_tifffile.rst.txt b/_sources/_build/large_image_source_tifffile/large_image_source_tifffile.rst.txt new file mode 100644 index 000000000..b569919db --- /dev/null +++ b/_sources/_build/large_image_source_tifffile/large_image_source_tifffile.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_tifffile package +====================================== + +Submodules +---------- + +large\_image\_source\_tifffile.girder\_source module +---------------------------------------------------- + +.. automodule:: large_image_source_tifffile.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_tifffile + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_tifffile/modules.rst.txt b/_sources/_build/large_image_source_tifffile/modules.rst.txt new file mode 100644 index 000000000..325ff39b1 --- /dev/null +++ b/_sources/_build/large_image_source_tifffile/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_tifffile +=========================== + +.. toctree:: + :maxdepth: 4 + + large_image_source_tifffile diff --git a/_sources/_build/large_image_source_vips/large_image_source_vips.rst.txt b/_sources/_build/large_image_source_vips/large_image_source_vips.rst.txt new file mode 100644 index 000000000..7aada020d --- /dev/null +++ b/_sources/_build/large_image_source_vips/large_image_source_vips.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_vips package +================================== + +Submodules +---------- + +large\_image\_source\_vips.girder\_source module +------------------------------------------------ + +.. automodule:: large_image_source_vips.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_vips + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_vips/modules.rst.txt b/_sources/_build/large_image_source_vips/modules.rst.txt new file mode 100644 index 000000000..4e848a6d1 --- /dev/null +++ b/_sources/_build/large_image_source_vips/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_vips +======================= + +.. toctree:: + :maxdepth: 4 + + large_image_source_vips diff --git a/_sources/_build/large_image_source_zarr/large_image_source_zarr.rst.txt b/_sources/_build/large_image_source_zarr/large_image_source_zarr.rst.txt new file mode 100644 index 000000000..75ae27541 --- /dev/null +++ b/_sources/_build/large_image_source_zarr/large_image_source_zarr.rst.txt @@ -0,0 +1,21 @@ +large\_image\_source\_zarr package +================================== + +Submodules +---------- + +large\_image\_source\_zarr.girder\_source module +------------------------------------------------ + +.. automodule:: large_image_source_zarr.girder_source + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_source_zarr + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_source_zarr/modules.rst.txt b/_sources/_build/large_image_source_zarr/modules.rst.txt new file mode 100644 index 000000000..832c50fb1 --- /dev/null +++ b/_sources/_build/large_image_source_zarr/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_source_zarr +======================= + +.. toctree:: + :maxdepth: 4 + + large_image_source_zarr diff --git a/_sources/_build/large_image_tasks/large_image_tasks.rst.txt b/_sources/_build/large_image_tasks/large_image_tasks.rst.txt new file mode 100644 index 000000000..984959801 --- /dev/null +++ b/_sources/_build/large_image_tasks/large_image_tasks.rst.txt @@ -0,0 +1,21 @@ +large\_image\_tasks package +=========================== + +Submodules +---------- + +large\_image\_tasks.tasks module +-------------------------------- + +.. automodule:: large_image_tasks.tasks + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: large_image_tasks + :members: + :undoc-members: + :show-inheritance: diff --git a/_sources/_build/large_image_tasks/modules.rst.txt b/_sources/_build/large_image_tasks/modules.rst.txt new file mode 100644 index 000000000..ed0f1b625 --- /dev/null +++ b/_sources/_build/large_image_tasks/modules.rst.txt @@ -0,0 +1,7 @@ +large_image_tasks +================= + +.. toctree:: + :maxdepth: 4 + + large_image_tasks diff --git a/_sources/annotations.rst.txt b/_sources/annotations.rst.txt new file mode 100644 index 000000000..5830ee620 --- /dev/null +++ b/_sources/annotations.rst.txt @@ -0,0 +1,6 @@ +.. include:: ../girder_annotation/docs/annotations.rst + +This returns the following: + +.. include:: ../build/docs-work/annotation_schema.json + :literal: diff --git a/_sources/api_index.rst.txt b/_sources/api_index.rst.txt new file mode 100644 index 000000000..4e994870a --- /dev/null +++ b/_sources/api_index.rst.txt @@ -0,0 +1,30 @@ +API Documentation +======================= + +The following pages are generated from module source code. + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + _build/large_image/modules + _build/large_image_source_bioformats/modules + _build/large_image_source_deepzoom/modules + _build/large_image_source_dicom/modules + _build/large_image_source_dummy/modules + _build/large_image_source_gdal/modules + _build/large_image_source_mapnik/modules + _build/large_image_source_multi/modules + _build/large_image_source_nd2/modules + _build/large_image_source_ometiff/modules + _build/large_image_source_openjpeg/modules + _build/large_image_source_openslide/modules + _build/large_image_source_pil/modules + _build/large_image_source_rasterio/modules + _build/large_image_source_test/modules + _build/large_image_source_tiff/modules + _build/large_image_source_tifffile/modules + _build/large_image_source_vips/modules + _build/large_image_source_zarr/modules + _build/large_image_converter/modules + _build/large_image_tasks/modules diff --git a/_sources/caching.rst.txt b/_sources/caching.rst.txt new file mode 100644 index 000000000..397417e38 --- /dev/null +++ b/_sources/caching.rst.txt @@ -0,0 +1,21 @@ +Caching in Large Image +====================== + +There are two main caches in large_image in addition to caching done on the operating system level. These are: + +- Tile Cache: The tile cache stores individual images and/or numpy arrays for each tile processed. + + - May be stored in the python process memory or memcached. Other cache backends can also be added. + - The cache key is a hash that includes the tile source, tile location within the source, format and compression, and style. + - If memcached is used, cached tiles can be shared across multiple processes. + - Tiles are often bigger than what memcached was optimized for, so memcached needs to be set to allow larger values. + - Cached tiles can include original as-read data as well as styled or transformed data. Tiles can be synthesized for sources that are missing specific resolutions; these are also cached. + - If using memcached, memcached determines how much memory is used (and what machine it is stored on). If using the python process, memory is limited to a fraction of total memory as reported by psutils. + +- Tile Source Cache: The source cache stores file handles, parsed metadata, and other values to optimize reading a specific large image. + + - Always stored in the python process memory (not shared between processes). + - Memory use is wildly different depending on tile source; an estimate is based on sample files and then the maximum number of sources that should be tiled is based on a frame of total memory as reported by psutils and this estimate. + - The cache key includes the tile source, default tile format, and style. + - File handles and other metadata are shared if sources only differ in style (for example if ICC color correction is applied in one and not in another). + - Because file handles are shared across sources that only differ in style, if a source implements a custom ``__del__`` operator, it needs to check if it is the unstyled source. diff --git a/_sources/config_options.rst.txt b/_sources/config_options.rst.txt new file mode 100644 index 000000000..cecac5283 --- /dev/null +++ b/_sources/config_options.rst.txt @@ -0,0 +1,192 @@ +Configuration Options +===================== + +Some functionality of large_image is controlled through configuration parameters. These can be read or set via python using functions in the ``large_image.config`` module, `getConfig <./_build/large_image/large_image.html#large_image.config.getConfig>`_ and `setConfig <./_build/large_image/large_image.html#large_image.config.setConfig>`_. + +.. list-table:: Configuration Parameters + :header-rows: 1 + :widths: 20 20 20 20 + + * - Key(s) + - Description + - Type + - Default + + .. _config_logger: + * - ``logger`` :ref:`🔗 ` + - Most log messages are sent here. + - ``logging.Logger`` + - This defaults to the standard python logger using the name large_image. When using Girder, this default to Girder's logger, which allows colored console output. + + .. _config_logprint: + * - ``logprint`` :ref:`🔗 ` + - Messages about available tilesources are sent here. + - ``logging.Logger`` + - This defaults to the standard python logger using the name large_image. When using Girder, this default to Girder's logger, which allows colored console output. + + .. _config_cache_backend: + * - ``cache_backend`` :ref:`🔗 ` + - String specifying how tiles are cached. If memcached is not available for any reason, the python cache is used instead. + - ``None | str: "python" | str: "memcached" | str: "redis"`` + - ``None`` (When None, the first cache available in the order memcached, redis, python is used. Otherwise, the specified cache is used if available, falling back to python if not.) + + .. _config_cache_python_memory_portion: + * - ``cache_python_memory_portion`` :ref:`🔗 ` + - If tiles are cached with python, the cache is sized so that it is expected to use less than 1 / (``cache_python_memory_portion``) of the available memory. + - ``int`` + - ``16`` + + .. _config_cache_memcached_url: + * - ``cache_memcached_url`` :ref:`🔗 ` + - If tiles are cached in memcached, the url or list of urls where the memcached server is located. + - ``str | List[str]`` + - ``"127.0.0.1"`` + + .. _config_cache_memcached_username: + * - ``cache_memcached_username`` :ref:`🔗 ` + - A username for the memcached server. + - ``str`` + - ``None`` + + .. _config_cache_memcached_password: + * - ``cache_memcached_password`` :ref:`🔗 ` + - A password for the memcached server. + - ``str`` + - ``None`` + + .. _config_cache_redis_url: + * - ``cache_redis_url`` :ref:`🔗 ` + - If tiles are cached in redis, the url or list of urls where the redis server is located. + - ``str | List[str]`` + - ``"127.0.0.1:6379"`` + + .. _config_cache_redis_username: + * - ``cache_redis_username`` :ref:`🔗 ` + - A username for the redis server. + - ``str`` + - ``None`` + + .. _config_cache_redis_password: + * - ``cache_redis_password`` :ref:`🔗 ` + - A password for the redis server. + - ``str`` + - ``None`` + + .. _config_cache_tilesource_memory_portion: + * - ``cache_tilesource_memory_portion`` :ref:`🔗 ` + - Tilesources are cached on open so that subsequent accesses can be faster. These use file handles and memory. This limits the maximum based on a memory estimation and using no more than 1 / (``cache_tilesource_memory_portion``) of the available memory. + - ``int`` + - ``32`` Memory usage by tile source is necessarily a rough estimate, since it can vary due to a wide variety of image-specific and deployment-specific details; this is intended to be conservative. + + .. _config_cache_tilesource_maximum: + * - ``cache_tilesource_maximum`` :ref:`🔗 ` + - If this is zero, this signifies that ``cache_tilesource_memory_portion`` determines the number of sources that will be cached. If this greater than 0, the cache will be the smaller of the value computed for the memory portion and this value (but always at least 3). + - ``int`` + - ``0`` + + .. _config_cache_sources: + * - ``cache_sources`` :ref:`🔗 ` + - If set to False, the default will be to not cache tile sources. This has substantial performance penalties if sources are used multiple times, so should only be set in singular dynamic environments such as experimental notebooks. + - ``bool`` + - ``True`` + + .. _config_max_small_image_size: + * - ``max_small_image_size`` :ref:`🔗 ` + - The PIL tilesource is used for small images if they are no more than this many pixels along their maximum dimension. + - ``int`` + - ``4096`` Specifying values greater than this could reduce compatibility with tile use on some browsers. In general, ``8192`` is safe for all modern systems, and values greater than ``16384`` should not be specified if the image will be viewed in any browser. + + .. _config_source_ignored_names: + * - ``source_bioformats_ignored_names``, + ``source_pil_ignored_names``, + ``source_vips_ignored_names`` :ref:`🔗 ` + - Some tile sources can read some files that are better read by other tilesources. Since reading these files is suboptimal, these tile sources have a setting that, by default, ignores files without extensions or with particular extensions. + - ``str`` (regular expression) + - Sources have different default values; see each source for its default. For example, the vips source default is ``r'(^[^.]*|\.(yml|yaml|json|png|svs|mrxs))$'`` + + .. _config_all_sources_ignored_names: + * - ``all_sources_ignored_names`` :ref:`🔗 ` + - If a file matches the regular expression in this setting, it will only be opened by sources that explicitly match the extension or mimetype. Some formats are composed of multiple files that can be read as either a small image or as a large image depending on the source; this prohibits all sources that don't explicitly support the format. + - ``str`` (regular expression) + - ``'(\.mrxs|\.vsi)$'`` + + .. _config_icc_correction: + * - ``icc_correction`` :ref:`🔗 ` + - If this is True or undefined, ICC color correction will be applied for tile sources that have ICC profile information. If False, correction will not be applied. If the style used to open a tilesource specifies ICC correction explicitly (on or off), then this setting is not used. This may also be a string with one of the intents defined by the PIL.ImageCms.Intents enum. ``True`` is the same as ``perceptual``. + - ``bool | str: one of PIL.ImageCms.Intents`` + - ``True`` + + .. _config_max_annotation_input_file_length: + * - ``max_annotation_input_file_length`` :ref:`🔗 ` + - When an annotation file is uploaded through Girder, it is loaded into memory, validated, and then added to the database. This is the maximum number of bytes that will be read directly. Files larger than this are ignored. + - ``int`` + - The larger of 1 GiByte and 1/16th of the system virtual memory + + +Configuration from Python +------------------------- + +As an example, configuration parameters can be set via python code like:: + + import large_image + + large_image.config.setConfig('max_small_image_size', 8192) + +If reading many different images but never revisiting them, it can be useful to reduce caching to a minimum. There is a utility function to make this easier:: + + large_image.config.minimizeCaching() + +Configuration from Environment +------------------------------ + +All configuration parameters can be specified as environment parameters by prefixing their uppercase names with ``LARGE_IMAGE_``. For instance, ``LARGE_IMAGE_CACHE_BACKEND=python`` specifies the cache backend. If the values can be decoded as json, they will be parsed as such. That is, numerical values will be parsed as numbers; to parse them as strings, surround them with double quotes. + +As another example, to use the least memory possible, set ``LARGE_IMAGE_CACHE_BACKEND=python LARGE_IMAGE_CACHE_PYTHON_MEMORY_PORTION=1000 LARGE_IMAGE_CACHE_TILESOURCE_MAXIMUM=2``. The first setting specifies caching tiles in the main process and not in memcached or an external cache. The second setting asks to use 1/1000th of the memory for a tile cache. The third settings caches no more than 2 tile sources (2 is the minimum). + +Configuration within the Girder Plugin +-------------------------------------- + +For the Girder plugin, these can also be set in the ``girder.cfg`` file in a ``large_image`` section. For example:: + + [large_image] + # cache_backend, used for caching tiles, is either "memcached" or "python" + cache_backend = "python" + # 'python' cache can use 1/(val) of the available memory + cache_python_memory_portion = 32 + # 'memcached' cache backend can specify the memcached server. + # cache_memcached_url may be a list + cache_memcached_url = "127.0.0.1" + cache_memcached_username = None + cache_memcached_password = None + # The tilesource cache uses the lesser of a value based on available file + # handles, the memory portion, and the maximum (if not 0) + cache_tilesource_memory_portion = 8 + cache_tilesource_maximum = 0 + # The PIL tilesource won't read images larger than the max small images size + max_small_image_size = 4096 + # The bioformats tilesource won't read files that end in a comma-separated + # list of extensions + source_bioformats_ignored_names = r'(^[!.]*|\.(jpg|jpeg|jpe|png|tif|tiff|ndpi))$' + # The maximum size of an annotation file that will be ingested into girder + # via direct load + max_annotation_input_file_length = 1 * 1024 ** 3 + +Logging from Python +------------------- + +The log levels can be adjusted in the standard Python manner:: + + import logging + import large_image + + logger = logging.getLogger('large_image') + logger.setLevel(logging.CRITICAL) + +Alternately, a different logger can be specified via ``setConfig`` in the ``logger`` and ``logprint`` settings:: + + import logging + import large_image + + logger = logging.getLogger(__name__) + large_image.config.setConfig('logger', logger) + large_image.config.setConfig('logprint', logger) diff --git a/_sources/development.rst.txt b/_sources/development.rst.txt new file mode 100644 index 000000000..199c1fe43 --- /dev/null +++ b/_sources/development.rst.txt @@ -0,0 +1,105 @@ +Developer Guide +=============== + +Developer Installation +---------------------- +To install all packages from source, clone the repository:: + + git clone https://github.com/girder/large_image.git + cd large_image + +Install all packages and dependencies:: + + pip install -e . -r requirements-dev.txt + +If you aren't developing with Girder 3, you can skip installing those components. Use ``requirements-dev-core.txt`` instead of ``requirements-dev.txt``:: + + pip install -e . -r requirements-dev-core.txt + + +Tile Source Requirements +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Many tile sources have complex prerequisites. These can be installed directly using your system's package manager or from some prebuilt Python wheels for Linux. The prebuilt wheels are not official packages, but they can be used by instructing pip to use them by preference:: + + pip install -e . -r requirements-dev.txt --find-links https://girder.github.io/large_image_wheels + + +Test Requirements +~~~~~~~~~~~~~~~~~~ + +Besides an appropriate version of Python, Large Image tests are run via `tox `_. This is also a convenient way to setup a development environment. + +The ``tox`` Python package must be installed: + +.. code-block:: bash + + pip install tox + +See the tox documentation for how to recreate test environments or perform other maintenance tasks. + +By default, instead of storing test environments in a ``.tox`` directory, they are stored in the ``build/tox`` directory. This is done for convenience in handling build artifacts from Girder-specific tests. + +nodejs and npm for Girder Tests or Development +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``nodejs`` version 14.x and a corresponding version of ``npm`` are required to build and test Girder client code. See `nodejs `_ for how to download and install it. Remember to get version 12 or 14. + +Mongo for Girder Tests or Development +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To run the full test suite, including Girder, ensure that a MongoDB instance is ready on ``localhost:27017``. This can be done with docker via ``docker run -p 27017:27017 -d mongo:latest``. + +Running Tests +------------- + +Tests are run via tox environments: + +.. code-block:: bash + + tox -e test-py39,lint,lintclient + +Or, without Girder: + +.. code-block:: bash + + tox -e core-py39,lint + +You can build the docs. They are created in the ``docs/build`` directory: + +.. code-block:: bash + + tox -e docs + +You can run specific tests using pytest's options, e.g., to try one specific test: + +.. code-block:: bash + + tox -e core-py39 -- -k testFromTiffRGBJPEG + + +Development Environment +----------------------- + +To set up a development environment, you can use tox. This is not required to run tests. The ``dev`` environment allows for complete use. The ``test`` environment will also install pytest and other tools needed for testing. Use the ``core`` environment instead of the ``dev`` environment if you aren't using Girder. + +For OSX users, specify the ``dev-osx`` environment instead; it will install only the cross-platform common sources. + +You can add a suffix to the environment to get a specific version of python (e.g., ``dev-py311`` or ``dev-osx-py310``. + +.. code-block:: bash + + tox --devenv /my/env/path -e dev + +and then switch to that environment: + +.. code-block:: bash + + . /my/env/path/bin/activate + +If you are using Girder, build and start it: + +.. code-block:: bash + + girder build --dev + girder serve diff --git a/_sources/dicomweb_assetstore.rst.txt b/_sources/dicomweb_assetstore.rst.txt new file mode 100644 index 000000000..b4e6270e8 --- /dev/null +++ b/_sources/dicomweb_assetstore.rst.txt @@ -0,0 +1 @@ +.. include:: ../sources/dicom/docs/dicomweb_assetstore.rst diff --git a/_sources/formats.rst.txt b/_sources/formats.rst.txt new file mode 100644 index 000000000..273e92560 --- /dev/null +++ b/_sources/formats.rst.txt @@ -0,0 +1,21 @@ +Image Formats +============= + +Preferred Extensions and Mime Types +----------------------------------- + +Images can generally be read regardless of their name. By default, when opening an image with ``large_image.open()``, each tile source reader is tried in turn until one source can open the file. Each source lists preferred file extensions and mime types with a priority level. If the file ends with one of these extensions or has one of these mimetypes, the order that the source readers are tried is adjusted based on the specified priority. + +The file extensions and mime types that are listed by the core sources that can affect source processing order are listed below. See ``large_image.listSources()`` for details about priority of the different source and the ``large_image.constants.SourcePriority`` for the priority meaning. + +Extensions +~~~~~~~~~~ + +.. include:: ../build/docs-work/known_extensions.txt + :literal: + +Mime Types +~~~~~~~~~~ + +.. include:: ../build/docs-work/known_mimetypes.txt + :literal: diff --git a/_sources/getting_started.rst.txt b/_sources/getting_started.rst.txt new file mode 100644 index 000000000..e970aa475 --- /dev/null +++ b/_sources/getting_started.rst.txt @@ -0,0 +1,396 @@ +Getting Started +=============== +The ``large_image`` library can be used to read and access different file formats. There are several common usage patterns. +To read more about accepted file formats, visit the :doc:`formats` page. + +These examples use ``sample.tiff`` as an example -- any readable image can be used in this case. Visit `demo.kitware.com `_ to download a sample image. + +Installation +------------ + +In addition to installing the base ``large-image`` package, you'll need at least one tile source which corresponds to your target file format(s) (a ``large-image-source-xxx`` package). You can install everything from the main project with one of these commands: + +Pip +~~~ + +Install common tile sources on linux, OSX, or Windows:: + + pip install large-image[common] + +Install all tile sources on linux:: + + pip install large-image[all] --find-links https://girder.github.io/large_image_wheels + +When using large-image with an instance of `Girder`_, install all tile sources and all Girder plugins on linux:: + + pip install large-image[all] girder-large-image-annotation[tasks] --find-links https://girder.github.io/large_image_wheels + + +Conda +~~~~~ + +Conda makes dependency management a bit easier if not on Linux. The base module, converter module, and two of the source modules are available on conda-forge. You can install the following:: + + conda install -c conda-forge large-image + conda install -c conda-forge large-image-source-gdal + conda install -c conda-forge large-image-source-tiff + conda install -c conda-forge large-image-converter + + +Reading Image Metadata +---------------------- + +All images have metadata that include the base image size, the base tile size, the number of conceptual levels, and information about the size of a pixel in the image if it is known. + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + print(source.getMetadata()) + +This might print a result like:: + + { + 'levels': 9, + 'sizeX': 58368, + 'sizeY': 12288, + 'tileWidth': 256, + 'tileHeight': 256, + 'magnification': 40.0, + 'mm_x': 0.00025, + 'mm_y': 0.00025 + } + +``levels`` doesn't actually tell which resolutions are present in the file. It is the number of levels that can be requested from the ``getTile`` method. The levels can also be computed via ``ceil(log(max(sizeX / tileWidth, sizeY / tileHeight)) / log(2)) + 1``. + +The ``mm_x`` and ``mm_y`` values are the size of a pixel in millimeters. These can be ``None`` if the value is unknown. The ``magnification`` is that reported by the file itself, and may be ``None``. The magnification can be approximated by ``0.01 / mm_x``. + +Getting a Region of an Image +---------------------------- + +You can get a portion of an image at different resolutions and in different formats. Internally, the large_image library reads the minimum amount of the file necessary to return the requested data, caching partial results in many instances so that a subsequent query may be faster. + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + image, mime_type = source.getRegion( + region=dict(left=1000, top=500, right=11000, bottom=1500), + output=dict(maxWidth=1000), + encoding='PNG') + # image is a PNG that is 1000 x 100. Specifically, it will be a bytes + # object that represent a PNG encoded image. + +You could also get this as a ``numpy`` array: + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + nparray, mime_type = source.getRegion( + region=dict(left=1000, top=500, right=11000, bottom=1500), + output=dict(maxWidth=1000), + format=large_image.constants.TILE_FORMAT_NUMPY) + # Our source image happens to be RGB, so nparray is a numpy array of shape + # (100, 1000, 3) + +You can specify the size in physical coordinates: + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + nparray, mime_type = source.getRegion( + region=dict(left=0.25, top=0.125, right=2.75, bottom=0.375, units='mm'), + scale=dict(mm_x=0.0025), + format=large_image.constants.TILE_FORMAT_NUMPY) + # Since our source image had mm_x = 0.00025 for its scale, this has the + # same result as the previous example. + +Tile Serving +------------ + +One of the uses of large_image is to get tiles that can be used in image or map viewers. Most of these viewers expect tiles that are a fixed size and known resolution. The ``getTile`` method returns tiles as stored in the original image and the original tile size. If there are missing levels, these are synthesized -- this is only done for missing powers-of-two levels or missing tiles. For instance, + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + # getTile takes x, y, z, where x and y are the tile location within the + # level and z is level where 0 is the lowest resolution. + tile0 = source.getTile(0, 0, 0) + # tile0 is the lowest resolution tile that shows the whole image. It will + # be a JPEG or PNG or some other image format depending on the source + tile002 = source.getTile(0, 0, 2) + # tile002 will be a tile representing no more than 1/4 the width of the + # image in the upper-left corner. Since the z (third parameter) is 2, the + # level will have up to 2**2 x 2**2 (4 x 4) tiles. An image doesn't + # necessarily have all tiles in that range, as the image may not be square. + +Some methods such as ``getRegion`` and ``getThumbnail`` allow you to specify format on the fly. But note that since tiles need to be cached in a consistent format, ``getTile`` always returns the same format depending on what encoding was specified when it was opened: + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff', encoding='PNG') + tile0 = source.getTile(0, 0, 0) + # tile is now guaranteed to be a PNG + +Tiles are always ``tileWidth`` by ``tileHeight`` in pixels. At the maximum level (``z = levels - 1``), the number of tiles in that level will range in ``x`` from ``0`` to strictly less than ``sizeX / tileWidth``, and ``y`` from ``0`` to strictly less than ``sizeY / tileHeight``. For each lower level, the is a power of two less tiles. For instance, when ``z = levels - 2``, ``x`` ranges from ``0`` to less than ``sizeX / tileWidth / 2``; at ``z = levels - 3``, ``x`` is less than ``sizeX / tileWidth / 4``. + +Iterating Across an Image +------------------------- + +Since most images are too large to conveniently fit in memory, it is useful to iterate through the image. +The ``tileIterator`` function can take the same parameters as ``getRegion`` to pick an output size and scale, but can also specify a tile size and overlap. +You can also get a specific tile with those parameters. This tiling doesn't have to have any correspondence to the tiling of the original file. +The data for each tile is loaded lazily, only once ``tile['tile']`` or ``tile['format']`` is accessed. + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + for tile in source.tileIterator( + tile_size=dict(width=512, height=512), + format=large_image.constants.TILE_FORMAT_NUMPY + ): + # tile is a dictionary of information about the specific tile + # tile['tile'] contains the actual numpy or image data + print(tile['x'], tile['y'], tile['tile'].shape) + # This will print something like: + # 0 0 (512, 512, 3) + # 512 0 (512, 512, 3) + # 1024 0 (512, 512, 3) + # ... + # 56832 11776 (512, 512, 3) + # 57344 11776 (512, 512, 3) + # 57856 11776 (512, 512, 3) + +You can overlap tiles. For instance, if you are running an algorithm where there are edge effects, you probably want an overlap that is big enough that you can trim off or ignore those effects: + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + for tile in source.tileIterator( + tile_size=dict(width=2048, height=2048), + tile_overlap=dict(x=128, y=128, edges=False), + format=large_image.constants.TILE_FORMAT_NUMPY + ): + print(tile['x'], tile['y'], tile['tile'].shape) + # This will print something like: + # 0 0 (2048, 2048, 3) + # 1920 0 (2048, 2048, 3) + # 3840 0 (2048, 2048, 3) + # ... + # 53760 11520 (768, 2048, 3) + # 55680 11520 (768, 2048, 3) + # 57600 11520 (768, 768, 3) + +Getting a Thumbnail +------------------- + +You can get a thumbnail of an image in different formats or resolutions. The default is typically JPEG and no larger than 256 x 256. Getting a thumbnail is essentially the same as doing ``getRegion``, except that it always uses the entire image and has a maximum width and/or height. + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + image, mime_type = source.getThumbnail() + open('thumb.jpg', 'wb').write(image) + +You can get the thumbnail in other image formats and sizes: + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + image, mime_type = source.getThumbnail(width=640, height=480, encoding='PNG') + open('thumb.png', 'wb').write(image) + +Associated Images +----------------- + +Many digital pathology images (also called whole slide images or WSI) contain secondary images that have additional information. This commonly includes label and macro images. A label image is a separate image of just the label of a slide. A macro image is a small image of the entire slide either including or excluding the label. There can be other associated images, too. + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff') + print(source.getAssociatedImagesList()) + # This prints something like: + # ['label', 'macro'] + image, mime_type = source.getAssociatedImage('macro') + # image is a binary image, such as a JPEG + image, mime_type = source.getAssociatedImage('macro', encoding='PNG') + # image is now a PNG + image, mime_type = source.getAssociatedImage('macro', format=large_image.constants.TILE_FORMAT_NUMPY) + # image is now a numpy array + +You can get associated images in different encodings and formats. The entire image is always returned. + +Projections +----------- + +large_image handles geospatial images. These can be handled as any other image in pixel-space by just opening them normally. Alternately, these can be opened with a new projection and then referenced using that projection. + +.. code-block:: python + + import large_image + # Open in Web Mercator projection + source = large_image.open('sample.geo.tiff', projection='EPSG:3857') + print(source.getMetadata()['bounds']) + # This will have the corners in Web Mercator meters, the projection, and + # the minimum and maximum ranges. + # We could also have done + print(source.getBounds()) + # The 0, 0, 0 tile is now the whole world excepting the poles + tile0 = source.getTile(0, 0, 0) + +Images with Multiple Frames +--------------------------- + +Some images have multiple "frames". Conceptually, these are images that could have multiple channels as separate images, such as those from fluorescence microscopy, multiple "z" values from serial sectioning of thick tissue or adjustment of focal plane in a microscope, multiple time ("t") values, or multiple regions of interest (frequently referred as "xy", "p", or "v" values). + +Any of the frames of such an image are accessed by adding a ``frame=`` parameter to the ``getTile``, ``getRegion``, ``tileIterator``, or other methods. + +.. code-block:: python + + import large_image + source = large_image.open('sample.ome.tiff') + print(source.getMetadata()) + # This will print something like + # { + # 'magnification': 8.130081300813009, + # 'mm_x': 0.00123, + # 'mm_y': 0.00123, + # 'sizeX': 2106, + # 'sizeY': 2016, + # 'tileHeight': 1024, + # 'tileWidth': 1024, + # 'IndexRange': {'IndexC': 3}, + # 'IndexStride': {'IndexC': 1}, + # 'frames': [ + # {'Frame': 0, 'Index': 0, 'IndexC': 0, 'IndexT': 0, 'IndexZ': 0}, + # {'Frame': 1, 'Index': 0, 'IndexC': 1, 'IndexT': 0, 'IndexZ': 0}, + # {'Frame': 2, 'Index': 0, 'IndexC': 2, 'IndexT': 0, 'IndexZ': 0} + # ] + # } + nparray, mime_type = source.getRegion( + frame=1, + format=large_image.constants.TILE_FORMAT_NUMPY) + # nparray will contain data from the middle channel image + +Channels, Bands, Samples, and Axes +---------------------------------- + +Various large image formats refer to channels, bands, and samples. This isn't consistent across different libraries. In an attempt to harmonize the geospatial and medical image terminology, large_image uses ``bands`` or ``samples`` to refer to image plane components, such as red, green, blue, and alpha. For geospatial data this can often have additional bands, such as near infrared or panchromatic. ``channels`` are stored as separate frames and can be interpreted as different imaging modalities. For example, a fluorescence microscopy image might have DAPI, CY5, and A594 channels. A common color photograph file has 3 bands (also called samples) and 1 channel. + +At times, image ``axes`` are used to indicate the order of data, especially when interpreted as an n-dimensional array. The ``x`` and ``y`` axes are the horizontal and vertical dimensions of the image. The ``s`` axis is the ``bands`` or ``samples``, such as red, green, and blue. The ``c`` axis is the ``channels`` with special support for channel names. This corresponds to distinct frames. + +The ``z`` and ``t`` are common enough that they are sometimes considered as primary axes. ``z`` corresponds to the direction orthogonal to ``x`` and ``y`` and is usually associated with altitude or microscope stage height. ``t`` is time. + +Other axes are supported provided their names are case-insensitively unique. + +Styles - Changing colors, scales, and other properties +------------------------------------------------------ + +By default, reading from an image gets the values stored in the image file. If you get a JPEG or PNG as the output, the values will be 8-bit per channel. If you get values as a numpy array, they will have their original resolution. Depending on the source image, this could be 16-bit per channel, floats, or other data types. + +Especially when working with high bit-depth images, it can be useful to modify the output. For example, you can adjust the color range: + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff', style={'min': 'min', 'max': 'max'}) + # now, any calls to getRegion, getTile, tileIterator, etc. will adjust the + # intensity so that the lowest value is mapped to black and the brightest + # value is mapped to white. + image, mime_type = source.getRegion( + region=dict(left=1000, top=500, right=11000, bottom=1500), + output=dict(maxWidth=1000)) + # image will use the full dynamic range + +You can also composite a multi-frame image into a false-color output: + +.. code-block:: python + + import large_image + source = large_image.open('sample.tiff', style={'bands': [ + {'frame': 0, 'min': 'min', 'max': 'max', 'palette': '#f00'}, + {'frame': 3, 'min': 'min', 'max': 'max', 'palette': '#0f0'}, + {'frame': 4, 'min': 'min', 'max': 'max', 'palette': '#00f'}, + ]}) + # Composite frames 0, 3, and 4 to red, green, and blue channels. + image, mime_type = source.getRegion( + region=dict(left=1000, top=500, right=11000, bottom=1500), + output=dict(maxWidth=1000)) + # image is false-color and full dynamic range of specific frames + +Writing an Image +---------------- + +If you wish to visualize numpy data, large_image can write a tiled tiff. This requires a tile source that supports writing to be installed. As of this writing, the ``large-image-source-zarr`` and ``large-image-source-vips`` sources supports this. If both are installed, the ``large-image-source-zarr`` is the default. + +.. code-block:: python + + import large_image + source = large_image.new() + for nparray, x, y in fancy_algorithm(): + # We could optionally add a mask to limit the output + source.addTile(nparray, x, y) + source.write('/tmp/sample.tiff', lossy=False) + +The ``large-image-source-zarr`` can be used to store multiple frame data with arbitrary axes. + +.. code-block:: python + + import large_image + source = large_image.new() + for nparray, x, y, time, param1 in fancy_algorithm(): + source.addTile(nparray, x, y, time=time, p1=param1) + # The writer supports a variety of formats + source.write('/tmp/sample.zarr.zip', lossy=False) + +You may also choose to read tiles from one source and write modified tiles to a new source: + +.. code-block:: python + + import large_image + original_source = large_image.open('path/to/original/image.tiff') + new_source = large_image.new() + for frame in original_source.getMetadata().get('frames', []): + for tile in original_source.tileIterator(frame=frame['Frame'], format='numpy'): + t, x, y = tile['tile'], tile['x'], tile['y'] + kwargs = { + 'z': frame['IndexZ'], + 'c': frame['IndexC'], + } + modified_tile = modify_tile(t) + new_source.addTile(modified_tile, x=x, y=y, **kwargs) + new_source.write('path/to/new/image.tiff', lossy=False) + +In some cases, it may be beneficial to write to a single image from multiple processes or threads: + +.. code-block:: python + + import large_image + import multiprocessing + # Important: Must be a pickleable function + def add_tile_to_source(tilesource, nparray, position): + tilesource.addTile( + nparray, + **position + ) + source = large_image.new() + # Important: Maximum size must be allocated before any multiprocess concurrency + add_tile_to_source(source, np.zeros(1, 1, 3), dict(x=max_x, y=max_y, z=max_z)) + # Also works with multiprocessing.ThreadPool, which does not need maximum size allocated first + with multiprocessing.Pool(max_workers=5) as pool: + pool.starmap( + add_tile_to_source, + [(source, t, t_pos) for t, t_pos in tileset] + ) + source.write('/tmp/sample.zarr.zip', lossy=False) + +.. _Girder: https://girder.readthedocs.io/en/latest/ diff --git a/_sources/girder_annotation_config_options.rst.txt b/_sources/girder_annotation_config_options.rst.txt new file mode 100644 index 000000000..07caefb38 --- /dev/null +++ b/_sources/girder_annotation_config_options.rst.txt @@ -0,0 +1,70 @@ +Girder Annotation Configuration Options +======================================= + +The ``large_image`` annotation plugin adds models to the Girder database for supporting annotating large images. These annotations can be rendered on images. +Annotations can include polygons, points, image overlays, and other types (see :doc:`annotations`). Each annotation can have a label and metadata. +Additional user interface libraries allow other libraries (like HistomicsUI) to let a user interactively add and modify annotations. + +General Plugin Settings +----------------------- + +There are some general plugin settings that affect large_image annotation as a Girder plugin. These settings can be accessed by an Admin user through the ``Admin Console`` / ``Plugins`` and selecting the gear icon next to ``Large image annotation``. + +Store annotation history +~~~~~~~~~~~~~~~~~~~~~~~~ + +If ``Record annotation history`` is selected, whenever annotations are saved, previous versions are kept in the database. This can greatly increase the size of the database. The old versions of the annotations allow the API to be used to revent to previous versions or to audit changes over time. + +.large_image_config.yaml +~~~~~~~~~~~~~~~~~~~~~~~~ + +This can be used to specify how annotations are listed on the item page. + +:: + + --- + # If present, show a table with column headers in annotation lists + annotationList: + # show these columns in order from left to right. Each column has a + # "type" and "value". It optionally has a "title" used for the column + # header, and a "format" used for searching and filtering. There are + # always control columns at the left and right. + columns: + - + # The "record" type is from the default annotation record. The value + # is one of "name", "creator", "created", "updatedId", "updated", + type: record + value: name + - + type: record + value: creator + # A format of user will print the user name instead of the id + format: user + - + type: record + value: created + # A format of date will use the browser's default date format + format: date + - + # The "metadata" type is taken from the annotations's + # "annotation.attributes" contents. It can be a nested key by using + # dots in its name. + type: metadata + value: Stain + # "format" can be "text", "number", "category". Other values may be + # specified later. + format: text + defaultSort: + # The default lists a sort order for sortable columns. This must have + # type, value, and dir for each entry, where dir is either "up" or + # "down". + - + type: metadata + value: Stain + dir: up + - + type: record + value: name + dir: down + +These values can be combined with values from the base large_image plugin. diff --git a/_sources/girder_caching.rst.txt b/_sources/girder_caching.rst.txt new file mode 100644 index 000000000..631283a7a --- /dev/null +++ b/_sources/girder_caching.rst.txt @@ -0,0 +1,65 @@ +Caching Large Image in Girder +============================= + +Tile sources are opened: when large image files uploaded or imported; when large image files are viewed; when thumbnails are generated; when an item page with a large image is viewd; and from some API calls. All of these result in the source being placed in the cache _except_ import. + +Since there are multiple users, the cache size should be large enough that no user has an image that they are actively viewing fall out of cache. + +Example of cache use when the ``GET`` ``/item/{id}/tile/zxy/{z}/{x}/{y}?style=",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=y.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=y.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),y.elements=c+" "+a,j(b)}function f(a){var b=x[a[v]];return b||(b={},w++,a[v]=w,x[w]=b),b}function g(a,c,d){if(c||(c=b),q)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():u.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||t.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),q)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return y.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(y,b.frag)}function j(a){a||(a=b);var d=f(a);return!y.shivCSS||p||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),q||i(a,d),a}function k(a){for(var b,c=a.getElementsByTagName("*"),e=c.length,f=RegExp("^(?:"+d().join("|")+")$","i"),g=[];e--;)b=c[e],f.test(b.nodeName)&&g.push(b.applyElement(l(b)));return g}function l(a){for(var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(A+":"+a.nodeName);d--;)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function m(a){for(var b,c=a.split("{"),e=c.length,f=RegExp("(^|[\\s,>+~])("+d().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),g="$1"+A+"\\:$2";e--;)b=c[e]=c[e].split("}"),b[b.length-1]=b[b.length-1].replace(f,g),c[e]=b.join("}");return c.join("{")}function n(a){for(var b=a.length;b--;)a[b].removeNode()}function o(a){function b(){clearTimeout(g._removeSheetTimer),d&&d.removeNode(!0),d=null}var d,e,g=f(a),h=a.namespaces,i=a.parentWindow;return!B||a.printShived?a:("undefined"==typeof h[A]&&h.add(A),i.attachEvent("onbeforeprint",function(){b();for(var f,g,h,i=a.styleSheets,j=[],l=i.length,n=Array(l);l--;)n[l]=i[l];for(;h=n.pop();)if(!h.disabled&&z.test(h.media)){try{f=h.imports,g=f.length}catch(o){g=0}for(l=0;g>l;l++)n.push(f[l]);try{j.push(h.cssText)}catch(o){}}j=m(j.reverse().join("")),e=k(a),d=c(a,j)}),i.attachEvent("onafterprint",function(){n(e),clearTimeout(g._removeSheetTimer),g._removeSheetTimer=setTimeout(b,500)}),a.printShived=!0,a)}var p,q,r="3.7.3",s=a.html5||{},t=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,u=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",w=0,x={};!function(){try{var a=b.createElement("a");a.innerHTML="",p="hidden"in a,q=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){p=!0,q=!0}}();var y={elements:s.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:r,shivCSS:s.shivCSS!==!1,supportsUnknownElements:q,shivMethods:s.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=y,j(b);var z=/^$|\b(?:all|print)\b/,A="html5shiv",B=!q&&function(){var c=b.documentElement;return!("undefined"==typeof b.namespaces||"undefined"==typeof b.parentWindow||"undefined"==typeof c.applyElement||"undefined"==typeof c.removeNode||"undefined"==typeof a.attachEvent)}();y.type+=" print",y.shivPrint=o,o(b),"object"==typeof module&&module.exports&&(module.exports=y)}("undefined"!=typeof window?window:this,document); \ No newline at end of file diff --git a/_static/js/html5shiv.min.js b/_static/js/html5shiv.min.js new file mode 100644 index 000000000..cd1c674f5 --- /dev/null +++ b/_static/js/html5shiv.min.js @@ -0,0 +1,4 @@ +/** +* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed +*/ +!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3-pre",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); \ No newline at end of file diff --git a/_static/js/theme.js b/_static/js/theme.js new file mode 100644 index 000000000..1fddb6ee4 --- /dev/null +++ b/_static/js/theme.js @@ -0,0 +1 @@ +!function(n){var e={};function t(i){if(e[i])return e[i].exports;var o=e[i]={i:i,l:!1,exports:{}};return n[i].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=n,t.c=e,t.d=function(n,e,i){t.o(n,e)||Object.defineProperty(n,e,{enumerable:!0,get:i})},t.r=function(n){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(n,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(n,"__esModule",{value:!0})},t.t=function(n,e){if(1&e&&(n=t(n)),8&e)return n;if(4&e&&"object"==typeof n&&n&&n.__esModule)return n;var i=Object.create(null);if(t.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:n}),2&e&&"string"!=typeof n)for(var o in n)t.d(i,o,function(e){return n[e]}.bind(null,o));return i},t.n=function(n){var e=n&&n.__esModule?function(){return n.default}:function(){return n};return t.d(e,"a",e),e},t.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},t.p="",t(t.s=0)}([function(n,e,t){t(1),n.exports=t(3)},function(n,e,t){(function(){var e="undefined"!=typeof window?window.jQuery:t(2);n.exports.ThemeNav={navBar:null,win:null,winScroll:!1,winResize:!1,linkScroll:!1,winPosition:0,winHeight:null,docHeight:null,isRunning:!1,enable:function(n){var t=this;void 0===n&&(n=!0),t.isRunning||(t.isRunning=!0,e((function(e){t.init(e),t.reset(),t.win.on("hashchange",t.reset),n&&t.win.on("scroll",(function(){t.linkScroll||t.winScroll||(t.winScroll=!0,requestAnimationFrame((function(){t.onScroll()})))})),t.win.on("resize",(function(){t.winResize||(t.winResize=!0,requestAnimationFrame((function(){t.onResize()})))})),t.onResize()})))},enableSticky:function(){this.enable(!0)},init:function(n){n(document);var e=this;this.navBar=n("div.wy-side-scroll:first"),this.win=n(window),n(document).on("click","[data-toggle='wy-nav-top']",(function(){n("[data-toggle='wy-nav-shift']").toggleClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift")})).on("click",".wy-menu-vertical .current ul li a",(function(){var t=n(this);n("[data-toggle='wy-nav-shift']").removeClass("shift"),n("[data-toggle='rst-versions']").toggleClass("shift"),e.toggleCurrent(t),e.hashChange()})).on("click","[data-toggle='rst-current-version']",(function(){n("[data-toggle='rst-versions']").toggleClass("shift-up")})),n("table.docutils:not(.field-list,.footnote,.citation)").wrap("
"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/_static/minus.png b/_static/minus.png new file mode 100644 index 000000000..d96755fda Binary files /dev/null and b/_static/minus.png differ diff --git a/_static/nbsphinx-broken-thumbnail.svg b/_static/nbsphinx-broken-thumbnail.svg new file mode 100644 index 000000000..4919ca882 --- /dev/null +++ b/_static/nbsphinx-broken-thumbnail.svg @@ -0,0 +1,9 @@ + + + + diff --git a/_static/nbsphinx-code-cells.css b/_static/nbsphinx-code-cells.css new file mode 100644 index 000000000..a3fb27c30 --- /dev/null +++ b/_static/nbsphinx-code-cells.css @@ -0,0 +1,259 @@ +/* remove conflicting styling from Sphinx themes */ +div.nbinput.container div.prompt *, +div.nboutput.container div.prompt *, +div.nbinput.container div.input_area pre, +div.nboutput.container div.output_area pre, +div.nbinput.container div.input_area .highlight, +div.nboutput.container div.output_area .highlight { + border: none; + padding: 0; + margin: 0; + box-shadow: none; +} + +div.nbinput.container > div[class*=highlight], +div.nboutput.container > div[class*=highlight] { + margin: 0; +} + +div.nbinput.container div.prompt *, +div.nboutput.container div.prompt * { + background: none; +} + +div.nboutput.container div.output_area .highlight, +div.nboutput.container div.output_area pre { + background: unset; +} + +div.nboutput.container div.output_area div.highlight { + color: unset; /* override Pygments text color */ +} + +/* avoid gaps between output lines */ +div.nboutput.container div[class*=highlight] pre { + line-height: normal; +} + +/* input/output containers */ +div.nbinput.container, +div.nboutput.container { + display: -webkit-flex; + display: flex; + align-items: flex-start; + margin: 0; + width: 100%; +} +@media (max-width: 540px) { + div.nbinput.container, + div.nboutput.container { + flex-direction: column; + } +} + +/* input container */ +div.nbinput.container { + padding-top: 5px; +} + +/* last container */ +div.nblast.container { + padding-bottom: 5px; +} + +/* input prompt */ +div.nbinput.container div.prompt pre, +/* for sphinx_immaterial theme: */ +div.nbinput.container div.prompt pre > code { + color: #307FC1; +} + +/* output prompt */ +div.nboutput.container div.prompt pre, +/* for sphinx_immaterial theme: */ +div.nboutput.container div.prompt pre > code { + color: #BF5B3D; +} + +/* all prompts */ +div.nbinput.container div.prompt, +div.nboutput.container div.prompt { + width: 4.5ex; + padding-top: 5px; + position: relative; + user-select: none; +} + +div.nbinput.container div.prompt > div, +div.nboutput.container div.prompt > div { + position: absolute; + right: 0; + margin-right: 0.3ex; +} + +@media (max-width: 540px) { + div.nbinput.container div.prompt, + div.nboutput.container div.prompt { + width: unset; + text-align: left; + padding: 0.4em; + } + div.nboutput.container div.prompt.empty { + padding: 0; + } + + div.nbinput.container div.prompt > div, + div.nboutput.container div.prompt > div { + position: unset; + } +} + +/* disable scrollbars and line breaks on prompts */ +div.nbinput.container div.prompt pre, +div.nboutput.container div.prompt pre { + overflow: hidden; + white-space: pre; +} + +/* input/output area */ +div.nbinput.container div.input_area, +div.nboutput.container div.output_area { + -webkit-flex: 1; + flex: 1; + overflow: auto; +} +@media (max-width: 540px) { + div.nbinput.container div.input_area, + div.nboutput.container div.output_area { + width: 100%; + } +} + +/* input area */ +div.nbinput.container div.input_area { + border: 1px solid #e0e0e0; + border-radius: 2px; + /*background: #f5f5f5;*/ +} + +/* override MathJax center alignment in output cells */ +div.nboutput.container div[class*=MathJax] { + text-align: left !important; +} + +/* override sphinx.ext.imgmath center alignment in output cells */ +div.nboutput.container div.math p { + text-align: left; +} + +/* standard error */ +div.nboutput.container div.output_area.stderr { + background: #fdd; +} + +/* ANSI colors */ +.ansi-black-fg { color: #3E424D; } +.ansi-black-bg { background-color: #3E424D; } +.ansi-black-intense-fg { color: #282C36; } +.ansi-black-intense-bg { background-color: #282C36; } +.ansi-red-fg { color: #E75C58; } +.ansi-red-bg { background-color: #E75C58; } +.ansi-red-intense-fg { color: #B22B31; } +.ansi-red-intense-bg { background-color: #B22B31; } +.ansi-green-fg { color: #00A250; } +.ansi-green-bg { background-color: #00A250; } +.ansi-green-intense-fg { color: #007427; } +.ansi-green-intense-bg { background-color: #007427; } +.ansi-yellow-fg { color: #DDB62B; } +.ansi-yellow-bg { background-color: #DDB62B; } +.ansi-yellow-intense-fg { color: #B27D12; } +.ansi-yellow-intense-bg { background-color: #B27D12; } +.ansi-blue-fg { color: #208FFB; } +.ansi-blue-bg { background-color: #208FFB; } +.ansi-blue-intense-fg { color: #0065CA; } +.ansi-blue-intense-bg { background-color: #0065CA; } +.ansi-magenta-fg { color: #D160C4; } +.ansi-magenta-bg { background-color: #D160C4; } +.ansi-magenta-intense-fg { color: #A03196; } +.ansi-magenta-intense-bg { background-color: #A03196; } +.ansi-cyan-fg { color: #60C6C8; } +.ansi-cyan-bg { background-color: #60C6C8; } +.ansi-cyan-intense-fg { color: #258F8F; } +.ansi-cyan-intense-bg { background-color: #258F8F; } +.ansi-white-fg { color: #C5C1B4; } +.ansi-white-bg { background-color: #C5C1B4; } +.ansi-white-intense-fg { color: #A1A6B2; } +.ansi-white-intense-bg { background-color: #A1A6B2; } + +.ansi-default-inverse-fg { color: #FFFFFF; } +.ansi-default-inverse-bg { background-color: #000000; } + +.ansi-bold { font-weight: bold; } +.ansi-underline { text-decoration: underline; } + + +div.nbinput.container div.input_area div[class*=highlight] > pre, +div.nboutput.container div.output_area div[class*=highlight] > pre, +div.nboutput.container div.output_area div[class*=highlight].math, +div.nboutput.container div.output_area.rendered_html, +div.nboutput.container div.output_area > div.output_javascript, +div.nboutput.container div.output_area:not(.rendered_html) > img{ + padding: 5px; + margin: 0; +} + +/* fix copybtn overflow problem in chromium (needed for 'sphinx_copybutton') */ +div.nbinput.container div.input_area > div[class^='highlight'], +div.nboutput.container div.output_area > div[class^='highlight']{ + overflow-y: hidden; +} + +/* hide copy button on prompts for 'sphinx_copybutton' extension ... */ +.prompt .copybtn, +/* ... and 'sphinx_immaterial' theme */ +.prompt .md-clipboard.md-icon { + display: none; +} + +/* Some additional styling taken form the Jupyter notebook CSS */ +.jp-RenderedHTMLCommon table, +div.rendered_html table { + border: none; + border-collapse: collapse; + border-spacing: 0; + color: black; + font-size: 12px; + table-layout: fixed; +} +.jp-RenderedHTMLCommon thead, +div.rendered_html thead { + border-bottom: 1px solid black; + vertical-align: bottom; +} +.jp-RenderedHTMLCommon tr, +.jp-RenderedHTMLCommon th, +.jp-RenderedHTMLCommon td, +div.rendered_html tr, +div.rendered_html th, +div.rendered_html td { + text-align: right; + vertical-align: middle; + padding: 0.5em 0.5em; + line-height: normal; + white-space: normal; + max-width: none; + border: none; +} +.jp-RenderedHTMLCommon th, +div.rendered_html th { + font-weight: bold; +} +.jp-RenderedHTMLCommon tbody tr:nth-child(odd), +div.rendered_html tbody tr:nth-child(odd) { + background: #f5f5f5; +} +.jp-RenderedHTMLCommon tbody tr:hover, +div.rendered_html tbody tr:hover { + background: rgba(66, 165, 245, 0.2); +} + diff --git a/_static/nbsphinx-gallery.css b/_static/nbsphinx-gallery.css new file mode 100644 index 000000000..365c27a96 --- /dev/null +++ b/_static/nbsphinx-gallery.css @@ -0,0 +1,31 @@ +.nbsphinx-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 5px; + margin-top: 1em; + margin-bottom: 1em; +} + +.nbsphinx-gallery > a { + padding: 5px; + border: 1px dotted currentColor; + border-radius: 2px; + text-align: center; +} + +.nbsphinx-gallery > a:hover { + border-style: solid; +} + +.nbsphinx-gallery img { + max-width: 100%; + max-height: 100%; +} + +.nbsphinx-gallery > a > div:first-child { + display: flex; + align-items: start; + justify-content: center; + height: 120px; + margin-bottom: 5px; +} diff --git a/_static/nbsphinx-no-thumbnail.svg b/_static/nbsphinx-no-thumbnail.svg new file mode 100644 index 000000000..9dca7588f --- /dev/null +++ b/_static/nbsphinx-no-thumbnail.svg @@ -0,0 +1,9 @@ + + + + diff --git a/_static/plus.png b/_static/plus.png new file mode 100644 index 000000000..7107cec93 Binary files /dev/null and b/_static/plus.png differ diff --git a/_static/pygments.css b/_static/pygments.css new file mode 100644 index 000000000..0d49244ed --- /dev/null +++ b/_static/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #eeffcc; } +.highlight .c { color: #408090; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #007020; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .ch { color: #408090; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #007020 } /* Comment.Preproc */ +.highlight .cpf { color: #408090; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #FF0000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #333333 } /* Generic.Output */ +.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #007020 } /* Keyword.Pseudo */ +.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #902000 } /* Keyword.Type */ +.highlight .m { color: #208050 } /* Literal.Number */ +.highlight .s { color: #4070a0 } /* Literal.String */ +.highlight .na { color: #4070a0 } /* Name.Attribute */ +.highlight .nb { color: #007020 } /* Name.Builtin */ +.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ +.highlight .no { color: #60add5 } /* Name.Constant */ +.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */ +.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #007020 } /* Name.Exception */ +.highlight .nf { color: #06287e } /* Name.Function */ +.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */ +.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #bb60d5 } /* Name.Variable */ +.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #208050 } /* Literal.Number.Bin */ +.highlight .mf { color: #208050 } /* Literal.Number.Float */ +.highlight .mh { color: #208050 } /* Literal.Number.Hex */ +.highlight .mi { color: #208050 } /* Literal.Number.Integer */ +.highlight .mo { color: #208050 } /* Literal.Number.Oct */ +.highlight .sa { color: #4070a0 } /* Literal.String.Affix */ +.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */ +.highlight .sc { color: #4070a0 } /* Literal.String.Char */ +.highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */ +.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4070a0 } /* Literal.String.Double */ +.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */ +.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ +.highlight .sx { color: #c65d09 } /* Literal.String.Other */ +.highlight .sr { color: #235388 } /* Literal.String.Regex */ +.highlight .s1 { color: #4070a0 } /* Literal.String.Single */ +.highlight .ss { color: #517918 } /* Literal.String.Symbol */ +.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #06287e } /* Name.Function.Magic */ +.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */ +.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ +.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ +.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ +.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/_static/searchtools.js b/_static/searchtools.js new file mode 100644 index 000000000..b08d58c9b --- /dev/null +++ b/_static/searchtools.js @@ -0,0 +1,620 @@ +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + "Search finished, found ${resultCount} page(s) matching the search query." + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/_static/sphinx_highlight.js b/_static/sphinx_highlight.js new file mode 100644 index 000000000..8a96c69a1 --- /dev/null +++ b/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/annotations.html b/annotations.html new file mode 100644 index 000000000..86ce008b4 --- /dev/null +++ b/annotations.html @@ -0,0 +1,1764 @@ + + + + + + + Annotation Schema — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Annotation Schema🔗

+

An annotation consists of a basic structure which includes a free-form +attributes object and a list of elements. The elements are +strictly specified by the schema and are mostly limited to a set of defined +shapes.

+

In addition to elements defined as shapes, image overlays are supported.

+

Partial annotations are shown below with some example values. Note that +the comments are not part of a valid annotation:

+
{
+  "name": "MyAnnotationName",              # Non-empty string.  Optional
+  "description": "This is a description",  # String.  Optional
+  "display": {                             # Object.  Optional
+      "visible": "new",                    # String or boolean.  Optional.
+                  # If "new", display this annotation when it first is added
+                  # to the system.  If false, don't display the annotation by
+                  # default.  If true, display the annotation when the item
+                  # is loaded.
+  },
+  "attributes": {                          # Object.  Optional
+    "key1": "value1",
+    "key2": ["any", {"value": "can"}, "go", "here"]
+  },
+  "elements": []                           # A list.  Optional.
+                                           # See below for valid elements.
+}
+
+
+
+

Elements🔗

+

Currently, most defined elements are shapes. Image overlays are not defined as +shapes. All of the shape elements have some properties that they are allowed. +Each element type is listed below:

+
+

All shapes🔗

+

All shapes have the following properties. If a property is not listed, +it is not allowed. If element IDs are specified, they must be unique.

+
{
+  "type": "point",                  # Exact string for the specific shape.  Required
+  "id": "0123456789abcdef01234567", # String, 24 lowercase hexadecimal digits.  Optional.
+  "label": {                        # Object.  Optional
+    "value": "This is a label",     # String.  Optional
+    "visibility": "hidden",         # String.  One of "always", "hidden", "onhover".  Optional
+    "fontSize": 3.4,                # Number.  Optional
+    "color": "#0000FF"              # String.  See note about colors.  Optional
+  },
+  "group": "group name",            # String. Optional
+  "user": {},                       # User properties -- this can contain anything,
+                                    # but should be kept small.  Optional.
+  <shape specific properties>
+}
+
+
+
+
+

All Vector Shapes🔗

+

These properties exist for all vector shapes (all but heatmaps, grid data, and image and pixelmap overlays).

+
{
+  "lineColor": "#000000",           # String.  See note about colors.  Optional
+  "lineWidth": 1,                   # Number >= 0.  Optional
+}
+
+
+
+
+

Circle🔗

+
{
+  "type": "circle",                  # Exact string.  Required
+  <id, label, group, user, lineColor, lineWidth>  # Optional general shape properties
+  "center": [10.3, -40.0, 0],        # Coordinate.  Required
+  "radius": 5.3,                     # Number >= 0.  Required
+  "fillColor": "#0000fF",            # String.  See note about colors.  Optional
+}
+
+
+
+
+

Ellipse🔗

+

The width and height of an ellipse are the major and minor axes.

+
{
+  "type": "ellipse",                 # Exact string.  Required
+  <id, label, group, user, lineColor, lineWidth>  # Optional general shape properties
+  "center": [10.3, -40.0, 0],        # Coordinate.  Required
+  "width": 5.3,                      # Number >= 0.  Required
+  "height": 17.3,                    # Number >= 0.  Required
+  "rotation": 0,                     # Number.  Counterclockwise radians around normal.  Required
+  "normal": [0, 0, 1.0],             # Three numbers specifying normal.  Default is positive Z.
+                                     # Optional
+  "fillColor": "rgba(0, 255, 0, 1)"  # String.  See note about colors.  Optional
+}
+
+
+
+
+

Point🔗

+
{
+  "type": "point",                   # Exact string.  Required
+  <id, label, group, user, lineColor, lineWidth>  # Optional general shape properties
+  "center": [123.3, 144.6, -123]     # Coordinate.  Required
+}
+
+
+
+
+

Polyline🔗

+

When closed, this is a polygon. When open, this is a continuous line.

+
{
+  "type": "polyline",                # Exact string.  Required
+  <id, label, group, user, lineColor, lineWidth>  # Optional general shape properties
+  "points": [                        # At least two points must be specified
+    [5,6,0],                         # Coordinate.  At least two required
+    [-17,6,0],
+    [56,-45,6]
+  ],
+  "closed": true,                    # Boolean.  Default is false.  Optional
+  "holes": [                         # Only used if closed is true.  A list of a list of
+                                     # coordinates.  Each list of coordinates is a
+                                     # separate hole within the main polygon, and is expected
+                                     # to be contained within it and not cross the main
+                                     # polygon or other holes.
+    [
+      [10,10,0],
+      [20,30,0],
+      [10,30,0]
+    ]
+  ],
+  "fillColor": "rgba(0, 255, 0, 1)"  # String.  See note about colors.  Optional
+}
+
+
+
+
+

Rectangle🔗

+
{
+  "type": "rectangle",               # Exact string.  Required
+  <id, label, group, user, lineColor, lineWidth>  # Optional general shape properties
+  "center": [10.3, -40.0, 0],        # Coordinate.  Required
+  "width": 5.3,                      # Number >= 0.  Required
+  "height": 17.3,                    # Number >= 0.  Required
+  "rotation": 0,                     # Number.  Counterclockwise radians around normal.  Required
+  "normal": [0, 0, 1.0],             # Three numbers specifying normal.  Default is positive Z.
+                                     # Optional
+  "fillColor": "rgba(0, 255, 0, 1)"  # String.  See note about colors.  Optional
+}
+
+
+
+
+

Heatmap🔗

+

A list of points with values that is interpreted as a heatmap so that +near by values aggregate together when viewed.

+
{
+  "type": "heatmap",                 # Exact string.  Required
+  <id, label, group, user>           # Optional general shape properties
+  "points": [                        # A list of coordinate-value entries.  Each is x, y, z, value.
+    [32320, 48416, 0, 0.192],
+    [40864, 109568, 0, 0.87],
+    [53472, 63392, 0, 0.262],
+    [23232, 96096, 0, 0.364],
+    [10976, 93376, 0, 0.2],
+    [42368, 65248, 0, 0.054]
+  ],
+  "radius": 25,                      # Positive number.  Optional.  The size of the gaussian point
+                                     # spread
+  "colorRange": ["rgba(0, 0, 0, 0)", "rgba(255, 255, 0, 1)"],  # A list of colors corresponding to
+                                     # the rangeValues.  Optional
+  "rangeValues": [0, 1],             # A list of range values corresponding to the colorRange list
+                                     # and possibly normalized to a scale of [0, 1].  Optional
+  "normalizeRange": true,            # If true, the rangeValues are normalized to [0, 1].  If
+                                     # false, the rangeValues are in the
+                                     # value domain.  Defaults to true.  Optional
+  "scaleWithZoom": true              # If true, scale the size of points with the zoom level of
+                                     # the map. Defaults to false. In this case, radius is in
+                                     # pixels of the associated image.  If false or unspecified,
+                                     # radius is in screen pixels. Optional
+}
+
+
+
+
+

Grid Data🔗

+

For evenly spaced data that is interpreted as a heatmap, contour, or +choropleth, a grid with a list of values can be specified.

+
{
+  "type": "griddata",                # Exact string.  Required
+  <id, label, group, user>           # Optional general shape properties
+  "interpretation": "contour",       # One of heatmap, contour, or choropleth
+  "gridWidth": 6,                    # Number of values across the grid.  Required
+  "origin": [0, 0, 0],               # Origin including fized x value.  Optional
+  "dx": 32,                          # Grid spacing in x.  Optional
+  "dy": 32,                          # Grid spacing in y.  Optional
+  "colorRange": ["rgba(0, 0, 0, 0)", "rgba(255, 255, 0, 1)"], # A list of colors corresponding to
+                                     # the rangeValues.  Optional
+  "rangeValues": [0, 1],             # A list of range values corresponding to the colorRange list.
+                                     # This should have the same number of entries as colorRange
+                                     # unless a contour where stepped is true.  Possibly normalized
+                                     # to a scale of [0, 1].  Optional
+  "normalizeRange": false,           # If true, the rangeValues are normalized to [0, 1].  If
+                                     # false, the rangeValues are in the value domain.  Defaults to
+                                     # true.  Optional
+  "minColor": "rgba(0, 0, 255, 1)",  # The color of data below the minimum range.  Optional
+  "maxColor": "rgba(255, 255, 0, 1)", # The color of data above the maximum range.  Optional
+  "stepped": true,                   # For contours, whether discrete colors or continuous colors
+                                     # should be used.  Default false.  Optional
+  "values": [
+    0.508,
+    0.806,
+    0.311,
+    0.402,
+    0.535,
+    0.661,
+    0.866,
+    0.31,
+    0.241,
+    0.63,
+    0.555,
+    0.067,
+    0.668,
+    0.164,
+    0.512,
+    0.647,
+    0.501,
+    0.637,
+    0.498,
+    0.658,
+    0.332,
+    0.431,
+    0.053,
+    0.531
+  ]
+}
+
+
+
+
+

Image overlays🔗

+

Image overlay annotations allow specifying a girder large image item +to display on top of the base image as an annotation. It supports +translation via the xoffset and yoffset properties, as well as other +types of transformations via its ‘matrix’ property which should be specified as +a 2x2 affine matrix.

+
{
+  "type": "image",                   # Exact string. Required
+  <id, label, group, user>           # Optional general shape properties
+  "girderId": <girder image id>,     # 24-character girder id pointing
+                                     # to a large image object. Required
+  "opacity": 1,                      # Default opacity for the overlay. Defaults to 1. Optional
+  "hasAlpha": false,                 # Boolean specifying if the image has an alpha channel
+                                     # that should be used in rendering.
+  "transform": {                     # Object specifying additional overlay information. Optional
+    "xoffset": 0,                    # How much to shift the overlaid image right.
+    "yoffset": 0,                    # How much to shift the overlaid image down.
+    "matrix": [                      # Affine matrix to specify transformations like scaling,
+                                     # rotation, or shearing.
+      [1, 0],
+      [0, 1]
+    ]
+  }
+}
+
+
+
+
+

Tiled pixelmap overlays🔗

+

Tiled pixelmap overlay annotations allow specifying a girder large +image item to display on top of the base image to help represent +categorical data. The specified large image overlay should be a +lossless tiled image where pixel values represent category indices +instead of colors. Data provided along with the ID of the image item +is used to color the pixelmap based on the categorical data.

+

The element must contain a values array. The indices of this +array correspond to pixel values on the pixelmap, and the values are +integers which correspond to indices in a categories array.

+
{
+  "type": "pixelmap",                # Exact string. Required
+  <id, label, group, user>           # Optional general shape properties
+  "girderId": <girder image id>,     # 24-character girder id pointing
+                                     # to a large image object. Required
+  "opacity": 1,                      # Default opacity for the overlay. Defaults to 1. Optional
+  "transform": {                     # Object specifying additional overlay information. Optional
+    "xoffset": 0,                    # How much to shift the overlaid image right.
+    "yoffset": 0,                    # How much to shift the overlaid image down.
+    "matrix": [                      # Affine matrix to specify transformations like scaling,
+                                     # rotation, or shearing.
+      [1, 0],
+      [0, 1]
+    ]
+  },
+  "boundaries": false,               # Whether boundaries within the pixelmap have unique values.
+                                     # If so, the values array should only be half as long as the
+                                     # actual number of distinct pixel values in the pixelmap. In
+                                     # this case, for a given index i in the values array, the
+                                     # pixels with value 2i will be given the corresponding
+                                     # fillColor from the category information, and the pixels
+                                     # with value 2i + 1 will be given the corresponding
+                                     # strokeColor from the category information. Required
+  "values": [                        # An array where the value at index 'i' is an integer
+                                     # pointing to an index in the categories array. Required
+      1,
+      2,
+      1,
+      1,
+      2,
+    ],
+    "categories": [                  # An array whose values contain category information.
+      {
+        "fillColor": "#0000FF",      # The color pixels with this category should be. Required
+        "label": "class_a",          # A human-readable label for this category. Optional
+      },
+      {
+        "fillColor": "#00FF00",
+        "label": "class_b",
+
+      },
+      {
+        "fillColor": "#FF0000",
+        "label": "class_c",
+      },
+  ]
+}
+
+
+
+
+

Arrow🔗

+

Not currently rendered.

+
{
+  "type": "arrow",                   # Exact string.  Required
+  <id, label, group, user, lineColor, lineWidth>  # Optional general shape properties
+  "points": [                        # Arrows ALWAYS have two points
+    [5,6,0],                         # Coordinate.  Arrow head.  Required
+    [-17,6,0]                        # Coordinate.  Aroow tail.  Required
+  ]
+}
+
+
+
+
+

Rectangle Grid🔗

+

Not currently rendered.

+

A Rectangle Grid is a rectangle which contains regular subdivisions, +such as that used to show a regular scale grid overlay on an image.

+
{
+  "type": "rectanglegrid",           # Exact string.  Required
+  <id, label, group, user, lineColor, lineWidth>  # Optional general shape properties
+  "center": [10.3, -40.0, 0],        # Coordinate.  Required
+  "width": 5.3,                      # Number >= 0.  Required
+  "height": 17.3,                    # Number >= 0.  Required
+  "rotation": 0,                     # Number.  Counterclockwise radians around normal.  Required
+  "normal": [0, 0, 1.0],             # Three numbers specifying normal.  Default is positive Z.
+                                     # Optional
+  "widthSubdivisions": 3,            # Integer > 0.  Required
+  "heightSubdivisions": 4,           # Integer > 0.  Required
+  "fillColor": "rgba(0, 255, 0, 1)"  # String.  See note about colors.  Optional
+}
+
+
+
+
+
+

Component Values🔗

+
+

Colors🔗

+

Colors are specified using a css-like string. Specifically, values of the form #RRGGBB, #RGB, #RRGGBBAA, and #RGBA are allowed where R, +G, B, and A are case-insensitive hexadecimal digits. Additionally, +values of the form rgb(123, 123, 123) and rgba(123, 123, 123, 0.123) +are allowed, where the colors are specified on a [0-255] integer scale, and +the opacity is specified as a [0-1] floating-point number.

+
+
+

Coordinates🔗

+

Coordinates are specified as a triplet of floating point numbers. They +are always three dimensional. As an example:

+

[1.3, -4.5, 0.3]

+
+
+
+

A sample annotation🔗

+

A sample that shows off a valid annotation:

+
{
+  "name": "AnnotationName",
+  "description": "This is a description",
+  "attributes": {
+    "key1": "value1",
+    "key2": ["any", {"value": "can"}, "go", "here"]
+  },
+  "elements": [{
+    "type": "point",
+    "label": {
+      "value": "This is a label",
+      "visibility": "hidden",
+      "fontSize": 3.4
+    },
+    "lineColor": "#000000",
+    "lineWidth": 1,
+    "center": [123.3, 144.6, -123]
+  },{
+    "type": "arrow",
+    "points": [
+      [5,6,0],
+      [-17,6,0]
+    ],
+    "lineColor": "rgba(128, 128, 128, 0.5)"
+  },{
+    "type": "circle",
+    "center": [10.3, -40.0, 0],
+    "radius": 5.3,
+    "fillColor": "#0000fF",
+    "lineColor": "rgb(3, 6, 8)"
+  },{
+    "type": "rectangle",
+    "center": [10.3, -40.0, 0],
+    "width": 5.3,
+    "height": 17.3,
+    "rotation": 0,
+    "fillColor": "rgba(0, 255, 0, 1)"
+  },{
+    "type": "ellipse",
+    "center": [3.53, 4.8, 0],
+    "width": 15.7,
+    "height": 7.1,
+    "rotation": 0.34,
+    "fillColor": "rgba(128, 255, 0, 0.5)"
+  },{
+    "type": "polyline",
+    "points": [
+      [5,6,0],
+      [-17,6,0],
+      [56,-45,6]
+    ],
+    "closed": true
+  },{
+    "type": "rectanglegrid",
+    "id": "0123456789abcdef01234567",
+    "center": [10.3, -40.0, 0],
+    "width": 5.3,
+    "height": 17.3,
+    "rotation": 0,
+    "widthSubdivisions": 3,
+    "heightSubdivisions": 4
+  }]
+}
+
+
+
+
+

Full Schema🔗

+

The full schema can be obtained by calling the Girder endpoint of +GET /annotation/schema.

+

This returns the following:

+
{
+  "$schema": "http://json-schema.org/schema#",
+  "type": "object",
+  "properties": {
+    "name": {
+      "type": "string",
+      "minLength": 1
+    },
+    "description": {
+      "type": "string"
+    },
+    "display": {
+      "type": "object",
+      "properties": {
+        "visible": {
+          "type": [
+            "boolean",
+            "string"
+          ],
+          "enum": [
+            "new",
+            true,
+            false
+          ],
+          "description": "This advises viewers on when the annotation should be shown.  If \"new\" (the default), show the annotation when it is first added to the system.  If false, don't show the annotation by default.  If true, show the annotation when the item is displayed."
+        }
+      }
+    },
+    "attributes": {
+      "type": "object",
+      "additionalProperties": true,
+      "title": "Image Attributes",
+      "description": "Subjective things that apply to the entire image."
+    },
+    "elements": {
+      "type": "array",
+      "items": {
+        "anyOf": [
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "arrow"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "lineColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "lineWidth": {
+                "type": "number",
+                "minimum": 0
+              },
+              "points": {
+                "type": "array",
+                "items": {
+                  "type": "array",
+                  "items": {
+                    "type": "number"
+                  },
+                  "minItems": 3,
+                  "maxItems": 3,
+                  "name": "Coordinate",
+                  "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+                },
+                "minItems": 2,
+                "maxItems": 2
+              },
+              "fillColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              }
+            },
+            "required": [
+              "points",
+              "type"
+            ],
+            "additionalProperties": false,
+            "description": "The first point is the head of the arrow"
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "circle"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "lineColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "lineWidth": {
+                "type": "number",
+                "minimum": 0
+              },
+              "center": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "radius": {
+                "type": "number",
+                "minimum": 0
+              },
+              "fillColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              }
+            },
+            "required": [
+              "center",
+              "radius",
+              "type"
+            ],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "ellipse"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "lineColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "lineWidth": {
+                "type": "number",
+                "minimum": 0
+              },
+              "center": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "width": {
+                "type": "number",
+                "minimum": 0
+              },
+              "height": {
+                "type": "number",
+                "minimum": 0
+              },
+              "rotation": {
+                "type": "number",
+                "description": "radians counterclockwise around normal"
+              },
+              "normal": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "fillColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              }
+            },
+            "required": [
+              "center",
+              "height",
+              "type",
+              "width"
+            ],
+            "additionalProperties": false,
+            "decription": "normal is the positive z-axis unless otherwise specified"
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "griddata"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "origin": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "dx": {
+                "type": "number",
+                "description": "grid spacing in the x direction"
+              },
+              "dy": {
+                "type": "number",
+                "description": "grid spacing in the y direction"
+              },
+              "gridWidth": {
+                "type": "integer",
+                "minimum": 1,
+                "description": "The number of values across the width of the grid"
+              },
+              "values": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "description": "The values of the grid.  This must have a multiple of gridWidth entries"
+              },
+              "interpretation": {
+                "type": "string",
+                "enum": [
+                  "heatmap",
+                  "contour",
+                  "choropleth"
+                ]
+              },
+              "radius": {
+                "type": "number",
+                "exclusiveMinimum": 0,
+                "description": "radius used for heatmap interpretation"
+              },
+              "colorRange": {
+                "type": "array",
+                "items": {
+                  "type": "string",
+                  "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                },
+                "description": "A list of colors"
+              },
+              "rangeValues": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "description": "A weakly monotonic list of range values"
+              },
+              "normalizeRange": {
+                "type": "boolean",
+                "description": "If true, rangeValues are on a scale of 0 to 1 and map to the minimum and maximum values on the data.  If false (the default), the rangeValues are the actual data values."
+              },
+              "stepped": {
+                "type": "boolean"
+              },
+              "minColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "maxColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              }
+            },
+            "required": [
+              "gridWidth",
+              "type",
+              "values"
+            ],
+            "additionalProperties": false,
+            "description": "ColorRange and rangeValues should have a one-to-one correspondence except for stepped contours where rangeValues needs one more entry than colorRange.  minColor and maxColor are the colors applies to values beyond the ranges in rangeValues."
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "heatmap"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "points": {
+                "type": "array",
+                "items": {
+                  "type": "array",
+                  "items": {
+                    "type": "number"
+                  },
+                  "minItems": 4,
+                  "maxItems": 4,
+                  "name": "CoordinateWithValue",
+                  "description": "An X, Y, Z, value coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+                }
+              },
+              "radius": {
+                "type": "number",
+                "exclusiveMinimum": 0
+              },
+              "colorRange": {
+                "type": "array",
+                "items": {
+                  "type": "string",
+                  "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                },
+                "description": "A list of colors"
+              },
+              "rangeValues": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "description": "A weakly monotonic list of range values"
+              },
+              "normalizeRange": {
+                "type": "boolean",
+                "description": "If true, rangeValues are on a scale of 0 to 1 and map to the minimum and maximum values on the data.  If false (the default), the rangeValues are the actual data values."
+              },
+              "scaleWithZoom": {
+                "type": "boolean",
+                "description": "If true, scale the size of points with the zoom level of the map."
+              }
+            },
+            "required": [
+              "points",
+              "type"
+            ],
+            "additionalProperties": false,
+            "description": "ColorRange and rangeValues should have a one-to-one correspondence."
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "point"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "lineColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "lineWidth": {
+                "type": "number",
+                "minimum": 0
+              },
+              "center": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "fillColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              }
+            },
+            "required": [
+              "center",
+              "type"
+            ],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "polyline"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "lineColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "lineWidth": {
+                "type": "number",
+                "minimum": 0
+              },
+              "points": {
+                "type": "array",
+                "items": {
+                  "type": "array",
+                  "items": {
+                    "type": "number"
+                  },
+                  "minItems": 3,
+                  "maxItems": 3,
+                  "name": "Coordinate",
+                  "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+                },
+                "minItems": 2
+              },
+              "fillColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "closed": {
+                "type": "boolean",
+                "description": "polyline is open if closed flag is not specified"
+              },
+              "holes": {
+                "type": "array",
+                "description": "If closed is true, this is a list of polylines that are treated as holes in the base polygon. These should not cross each other and should be contained within the base polygon.",
+                "items": {
+                  "type": "array",
+                  "items": {
+                    "type": "array",
+                    "items": {
+                      "type": "number"
+                    },
+                    "minItems": 3,
+                    "maxItems": 3,
+                    "name": "Coordinate",
+                    "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+                  },
+                  "minItems": 3
+                }
+              }
+            },
+            "required": [
+              "points",
+              "type"
+            ],
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "rectangle"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "lineColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "lineWidth": {
+                "type": "number",
+                "minimum": 0
+              },
+              "center": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "width": {
+                "type": "number",
+                "minimum": 0
+              },
+              "height": {
+                "type": "number",
+                "minimum": 0
+              },
+              "rotation": {
+                "type": "number",
+                "description": "radians counterclockwise around normal"
+              },
+              "normal": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "fillColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              }
+            },
+            "required": [
+              "center",
+              "height",
+              "type",
+              "width"
+            ],
+            "additionalProperties": false,
+            "decription": "normal is the positive z-axis unless otherwise specified"
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "rectanglegrid"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "lineColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "lineWidth": {
+                "type": "number",
+                "minimum": 0
+              },
+              "center": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "width": {
+                "type": "number",
+                "minimum": 0
+              },
+              "height": {
+                "type": "number",
+                "minimum": 0
+              },
+              "rotation": {
+                "type": "number",
+                "description": "radians counterclockwise around normal"
+              },
+              "normal": {
+                "type": "array",
+                "items": {
+                  "type": "number"
+                },
+                "minItems": 3,
+                "maxItems": 3,
+                "name": "Coordinate",
+                "description": "An X, Y, Z coordinate tuple, in base layer pixel coordinates, where the origin is the upper-left."
+              },
+              "fillColor": {
+                "type": "string",
+                "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+              },
+              "widthSubdivisions": {
+                "type": "integer",
+                "minimum": 1
+              },
+              "heightSubdivisions": {
+                "type": "integer",
+                "minimum": 1
+              }
+            },
+            "required": [
+              "center",
+              "height",
+              "heightSubdivisions",
+              "type",
+              "width",
+              "widthSubdivisions"
+            ],
+            "additionalProperties": false,
+            "decription": "normal is the positive z-axis unless otherwise specified"
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "image"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "girderId": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$",
+                "description": "Girder item ID containing the image to overlay."
+              },
+              "opacity": {
+                "type": "number",
+                "minimum": 0,
+                "maximum": 1,
+                "description": "Default opacity for this image overlay. Must be between 0 and 1. Defaults to 1."
+              },
+              "hasAlpha": {
+                "type": "boolean",
+                "description": "If true, the image is treated assuming it has an alpha channel."
+              },
+              "transform": {
+                "type": "object",
+                "description": "Specification for an affine transform of the image overlay. Includes a 2D transform matrix, an X offset and a Y offset.",
+                "properties": {
+                  "xoffset": {
+                    "type": "number"
+                  },
+                  "yoffset": {
+                    "type": "number"
+                  },
+                  "matrix": {
+                    "type": "array",
+                    "items": {
+                      "type": "array",
+                      "minItems": 2,
+                      "maxItems": 2
+                    },
+                    "minItems": 2,
+                    "maxItems": 2,
+                    "description": "A 2D matrix representing the transform of an image overlay."
+                  }
+                }
+              }
+            },
+            "required": [
+              "girderId",
+              "type"
+            ],
+            "additionalProperties": false,
+            "description": "An image overlay on top of the base resource."
+          },
+          {
+            "type": "object",
+            "properties": {
+              "id": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$"
+              },
+              "type": {
+                "type": "string",
+                "enum": [
+                  "pixelmap"
+                ]
+              },
+              "user": {
+                "type": "object",
+                "additionalProperties": true
+              },
+              "label": {
+                "type": "object",
+                "properties": {
+                  "value": {
+                    "type": "string"
+                  },
+                  "visibility": {
+                    "type": "string",
+                    "enum": [
+                      "hidden",
+                      "always",
+                      "onhover"
+                    ]
+                  },
+                  "fontSize": {
+                    "type": "number",
+                    "exclusiveMinimum": 0
+                  },
+                  "color": {
+                    "type": "string",
+                    "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                  }
+                },
+                "required": [
+                  "value"
+                ],
+                "additionalProperties": false
+              },
+              "group": {
+                "type": "string"
+              },
+              "girderId": {
+                "type": "string",
+                "pattern": "^[0-9a-f]{24}$",
+                "description": "Girder item ID containing the image to overlay."
+              },
+              "opacity": {
+                "type": "number",
+                "minimum": 0,
+                "maximum": 1,
+                "description": "Default opacity for this image overlay. Must be between 0 and 1. Defaults to 1."
+              },
+              "hasAlpha": {
+                "type": "boolean",
+                "description": "If true, the image is treated assuming it has an alpha channel."
+              },
+              "transform": {
+                "type": "object",
+                "description": "Specification for an affine transform of the image overlay. Includes a 2D transform matrix, an X offset and a Y offset.",
+                "properties": {
+                  "xoffset": {
+                    "type": "number"
+                  },
+                  "yoffset": {
+                    "type": "number"
+                  },
+                  "matrix": {
+                    "type": "array",
+                    "items": {
+                      "type": "array",
+                      "minItems": 2,
+                      "maxItems": 2
+                    },
+                    "minItems": 2,
+                    "maxItems": 2,
+                    "description": "A 2D matrix representing the transform of an image overlay."
+                  }
+                }
+              },
+              "values": {
+                "type": "array",
+                "items": {
+                  "type": "integer"
+                },
+                "description": "An array where the indices correspond to pixel values in the pixel map image and the values are used to look up the appropriate color in the categories property."
+              },
+              "categories": {
+                "type": "array",
+                "items": {
+                  "type": "object",
+                  "properties": {
+                    "fillColor": {
+                      "type": "string",
+                      "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                    },
+                    "strokeColor": {
+                      "type": "string",
+                      "pattern": "^(#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgb\\(\\d+,\\s*\\d+,\\s*\\d+\\)|rgba\\(\\d+,\\s*\\d+,\\s*\\d+,\\s*(\\d?\\.|)\\d+\\))$"
+                    },
+                    "label": {
+                      "type": "string",
+                      "description": "A string representing the semantic meaning of regions of the map with the corresponding color."
+                    },
+                    "description": {
+                      "type": "string",
+                      "description": "A more detailed explanation of the meaining of this category."
+                    }
+                  },
+                  "required": [
+                    "fillColor"
+                  ],
+                  "additionalProperties": false
+                },
+                "description": "An array used to map between the values array and color values. Can also contain semantic information for color values."
+              },
+              "boundaries": {
+                "type": "boolean",
+                "description": "True if the pixelmap doubles pixel values such that even values are the fill and odd values the are stroke of each superpixel. If true, the length of the values array should be half of the maximum value in the pixelmap."
+              }
+            },
+            "required": [
+              "boundaries",
+              "categories",
+              "girderId",
+              "type",
+              "values"
+            ],
+            "additionalProperties": false,
+            "description": "A tiled pixelmap to overlay onto a base resource."
+          }
+        ]
+      },
+      "title": "Image Markup",
+      "description": "Subjective things that apply to a spatial region."
+    }
+  },
+  "additionalProperties": false
+}
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/api_index.html b/api_index.html new file mode 100644 index 000000000..1897dcb5d --- /dev/null +++ b/api_index.html @@ -0,0 +1,174 @@ + + + + + + + API Documentation — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/caching.html b/caching.html new file mode 100644 index 000000000..19d9d1a6b --- /dev/null +++ b/caching.html @@ -0,0 +1,146 @@ + + + + + + + Caching in Large Image — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Caching in Large Image🔗

+

There are two main caches in large_image in addition to caching done on the operating system level. These are:

+
    +
  • Tile Cache: The tile cache stores individual images and/or numpy arrays for each tile processed.

    +
      +
    • May be stored in the python process memory or memcached. Other cache backends can also be added.

    • +
    • The cache key is a hash that includes the tile source, tile location within the source, format and compression, and style.

    • +
    • If memcached is used, cached tiles can be shared across multiple processes.

    • +
    • Tiles are often bigger than what memcached was optimized for, so memcached needs to be set to allow larger values.

    • +
    • Cached tiles can include original as-read data as well as styled or transformed data. Tiles can be synthesized for sources that are missing specific resolutions; these are also cached.

    • +
    • If using memcached, memcached determines how much memory is used (and what machine it is stored on). If using the python process, memory is limited to a fraction of total memory as reported by psutils.

    • +
    +
  • +
  • Tile Source Cache: The source cache stores file handles, parsed metadata, and other values to optimize reading a specific large image.

    +
      +
    • Always stored in the python process memory (not shared between processes).

    • +
    • Memory use is wildly different depending on tile source; an estimate is based on sample files and then the maximum number of sources that should be tiled is based on a frame of total memory as reported by psutils and this estimate.

    • +
    • The cache key includes the tile source, default tile format, and style.

    • +
    • File handles and other metadata are shared if sources only differ in style (for example if ICC color correction is applied in one and not in another).

    • +
    • Because file handles are shared across sources that only differ in style, if a source implements a custom __del__ operator, it needs to check if it is the unstyled source.

    • +
    +
  • +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/config_options.html b/config_options.html new file mode 100644 index 000000000..8c0bf1c74 --- /dev/null +++ b/config_options.html @@ -0,0 +1,325 @@ + + + + + + + Configuration Options — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Configuration Options🔗

+

Some functionality of large_image is controlled through configuration parameters. These can be read or set via python using functions in the large_image.config module, getConfig and setConfig.

+ + ++++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Configuration Parameters🔗

Key(s)

Description

Type

Default

+

logger 🔗

Most log messages are sent here.

logging.Logger

This defaults to the standard python logger using the name large_image. When using Girder, this default to Girder’s logger, which allows colored console output.

+

logprint 🔗

Messages about available tilesources are sent here.

logging.Logger

This defaults to the standard python logger using the name large_image. When using Girder, this default to Girder’s logger, which allows colored console output.

+

cache_backend 🔗

String specifying how tiles are cached. If memcached is not available for any reason, the python cache is used instead.

None | str: "python" | str: "memcached" | str: "redis"

None (When None, the first cache available in the order memcached, redis, python is used. Otherwise, the specified cache is used if available, falling back to python if not.)

+

cache_python_memory_portion 🔗

If tiles are cached with python, the cache is sized so that it is expected to use less than 1 / (cache_python_memory_portion) of the available memory.

int

16

+

cache_memcached_url 🔗

If tiles are cached in memcached, the url or list of urls where the memcached server is located.

str | List[str]

"127.0.0.1"

+

cache_memcached_username 🔗

A username for the memcached server.

str

None

+

cache_memcached_password 🔗

A password for the memcached server.

str

None

+

cache_redis_url 🔗

If tiles are cached in redis, the url or list of urls where the redis server is located.

str | List[str]

"127.0.0.1:6379"

+

cache_redis_username 🔗

A username for the redis server.

str

None

+

cache_redis_password 🔗

A password for the redis server.

str

None

+

cache_tilesource_memory_portion 🔗

Tilesources are cached on open so that subsequent accesses can be faster. These use file handles and memory. This limits the maximum based on a memory estimation and using no more than 1 / (cache_tilesource_memory_portion) of the available memory.

int

32 Memory usage by tile source is necessarily a rough estimate, since it can vary due to a wide variety of image-specific and deployment-specific details; this is intended to be conservative.

+

cache_tilesource_maximum 🔗

If this is zero, this signifies that cache_tilesource_memory_portion determines the number of sources that will be cached. If this greater than 0, the cache will be the smaller of the value computed for the memory portion and this value (but always at least 3).

int

0

+

cache_sources 🔗

If set to False, the default will be to not cache tile sources. This has substantial performance penalties if sources are used multiple times, so should only be set in singular dynamic environments such as experimental notebooks.

bool

True

+

max_small_image_size 🔗

The PIL tilesource is used for small images if they are no more than this many pixels along their maximum dimension.

int

4096 Specifying values greater than this could reduce compatibility with tile use on some browsers. In general, 8192 is safe for all modern systems, and values greater than 16384 should not be specified if the image will be viewed in any browser.

+

source_bioformats_ignored_names, +source_pil_ignored_names, +source_vips_ignored_names 🔗

Some tile sources can read some files that are better read by other tilesources. Since reading these files is suboptimal, these tile sources have a setting that, by default, ignores files without extensions or with particular extensions.

str (regular expression)

Sources have different default values; see each source for its default. For example, the vips source default is r'(^[^.]*|\.(yml|yaml|json|png|svs|mrxs))$'

+

all_sources_ignored_names 🔗

If a file matches the regular expression in this setting, it will only be opened by sources that explicitly match the extension or mimetype. Some formats are composed of multiple files that can be read as either a small image or as a large image depending on the source; this prohibits all sources that don’t explicitly support the format.

str (regular expression)

'(\.mrxs|\.vsi)$'

+

icc_correction 🔗

If this is True or undefined, ICC color correction will be applied for tile sources that have ICC profile information. If False, correction will not be applied. If the style used to open a tilesource specifies ICC correction explicitly (on or off), then this setting is not used. This may also be a string with one of the intents defined by the PIL.ImageCms.Intents enum. True is the same as perceptual.

bool | str: one of PIL.ImageCms.Intents

True

+

max_annotation_input_file_length 🔗

When an annotation file is uploaded through Girder, it is loaded into memory, validated, and then added to the database. This is the maximum number of bytes that will be read directly. Files larger than this are ignored.

int

The larger of 1 GiByte and 1/16th of the system virtual memory

+
+

Configuration from Python🔗

+

As an example, configuration parameters can be set via python code like:

+
import large_image
+
+large_image.config.setConfig('max_small_image_size', 8192)
+
+
+

If reading many different images but never revisiting them, it can be useful to reduce caching to a minimum. There is a utility function to make this easier:

+
large_image.config.minimizeCaching()
+
+
+
+
+

Configuration from Environment🔗

+

All configuration parameters can be specified as environment parameters by prefixing their uppercase names with LARGE_IMAGE_. For instance, LARGE_IMAGE_CACHE_BACKEND=python specifies the cache backend. If the values can be decoded as json, they will be parsed as such. That is, numerical values will be parsed as numbers; to parse them as strings, surround them with double quotes.

+

As another example, to use the least memory possible, set LARGE_IMAGE_CACHE_BACKEND=python LARGE_IMAGE_CACHE_PYTHON_MEMORY_PORTION=1000 LARGE_IMAGE_CACHE_TILESOURCE_MAXIMUM=2. The first setting specifies caching tiles in the main process and not in memcached or an external cache. The second setting asks to use 1/1000th of the memory for a tile cache. The third settings caches no more than 2 tile sources (2 is the minimum).

+
+
+

Configuration within the Girder Plugin🔗

+

For the Girder plugin, these can also be set in the girder.cfg file in a large_image section. For example:

+
[large_image]
+# cache_backend, used for caching tiles, is either "memcached" or "python"
+cache_backend = "python"
+# 'python' cache can use 1/(val) of the available memory
+cache_python_memory_portion = 32
+# 'memcached' cache backend can specify the memcached server.
+# cache_memcached_url may be a list
+cache_memcached_url = "127.0.0.1"
+cache_memcached_username = None
+cache_memcached_password = None
+# The tilesource cache uses the lesser of a value based on available file
+# handles, the memory portion, and the maximum (if not 0)
+cache_tilesource_memory_portion = 8
+cache_tilesource_maximum = 0
+# The PIL tilesource won't read images larger than the max small images size
+max_small_image_size = 4096
+# The bioformats tilesource won't read files that end in a comma-separated
+# list of extensions
+source_bioformats_ignored_names = r'(^[!.]*|\.(jpg|jpeg|jpe|png|tif|tiff|ndpi))$'
+# The maximum size of an annotation file that will be ingested into girder
+# via direct load
+max_annotation_input_file_length = 1 * 1024 ** 3
+
+
+
+
+

Logging from Python🔗

+

The log levels can be adjusted in the standard Python manner:

+
import logging
+import large_image
+
+logger = logging.getLogger('large_image')
+logger.setLevel(logging.CRITICAL)
+
+
+

Alternately, a different logger can be specified via setConfig in the logger and logprint settings:

+
import logging
+import large_image
+
+logger = logging.getLogger(__name__)
+large_image.config.setConfig('logger', logger)
+large_image.config.setConfig('logprint', logger)
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/development.html b/development.html new file mode 100644 index 000000000..a765117e6 --- /dev/null +++ b/development.html @@ -0,0 +1,213 @@ + + + + + + + Developer Guide — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Developer Guide🔗

+
+

Developer Installation🔗

+

To install all packages from source, clone the repository:

+
git clone https://github.com/girder/large_image.git
+cd large_image
+
+
+

Install all packages and dependencies:

+
pip install -e . -r requirements-dev.txt
+
+
+

If you aren’t developing with Girder 3, you can skip installing those components. Use requirements-dev-core.txt instead of requirements-dev.txt:

+
pip install -e . -r requirements-dev-core.txt
+
+
+
+

Tile Source Requirements🔗

+

Many tile sources have complex prerequisites. These can be installed directly using your system’s package manager or from some prebuilt Python wheels for Linux. The prebuilt wheels are not official packages, but they can be used by instructing pip to use them by preference:

+
pip install -e . -r requirements-dev.txt --find-links https://girder.github.io/large_image_wheels
+
+
+
+
+

Test Requirements🔗

+

Besides an appropriate version of Python, Large Image tests are run via tox. This is also a convenient way to setup a development environment.

+

The tox Python package must be installed:

+
pip install tox
+
+
+

See the tox documentation for how to recreate test environments or perform other maintenance tasks.

+

By default, instead of storing test environments in a .tox directory, they are stored in the build/tox directory. This is done for convenience in handling build artifacts from Girder-specific tests.

+
+
+

nodejs and npm for Girder Tests or Development🔗

+

nodejs version 14.x and a corresponding version of npm are required to build and test Girder client code. See nodejs for how to download and install it. Remember to get version 12 or 14.

+
+
+

Mongo for Girder Tests or Development🔗

+

To run the full test suite, including Girder, ensure that a MongoDB instance is ready on localhost:27017. This can be done with docker via docker run -p 27017:27017 -d mongo:latest.

+
+
+
+

Running Tests🔗

+

Tests are run via tox environments:

+
tox -e test-py39,lint,lintclient
+
+
+

Or, without Girder:

+
tox -e core-py39,lint
+
+
+

You can build the docs. They are created in the docs/build directory:

+
tox -e docs
+
+
+

You can run specific tests using pytest’s options, e.g., to try one specific test:

+
tox -e core-py39 -- -k testFromTiffRGBJPEG
+
+
+
+
+

Development Environment🔗

+

To set up a development environment, you can use tox. This is not required to run tests. The dev environment allows for complete use. The test environment will also install pytest and other tools needed for testing. Use the core environment instead of the dev environment if you aren’t using Girder.

+

For OSX users, specify the dev-osx environment instead; it will install only the cross-platform common sources.

+

You can add a suffix to the environment to get a specific version of python (e.g., dev-py311 or dev-osx-py310.

+
tox --devenv /my/env/path -e dev
+
+
+

and then switch to that environment:

+
. /my/env/path/bin/activate
+
+
+

If you are using Girder, build and start it:

+
girder build --dev
+girder serve
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/dicomweb_assetstore.html b/dicomweb_assetstore.html new file mode 100644 index 000000000..576a59bcf --- /dev/null +++ b/dicomweb_assetstore.html @@ -0,0 +1,138 @@ + + + + + + + DICOMweb Assetstore — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

DICOMweb Assetstore🔗

+

The DICOM source also provides a Girder Assetstore plugin for accessing data on DICOMweb servers. This is available if the package is installed in the same python environment as Girder and the Girder client is built after installation.

+

A DICOMweb assetstore can be added through the Girder Admin Console by selecting the “Create new DICOMweb assetstore” button. It requires an appropriate URL; for those using DCM4CHEE, this might be something like “https://some.server.com/dcm4chee-arc/aets/DCM4CHEE/rs”. Check with the DICOMweb provider for appropriate QIDO and WADO prefixes.

+

Existing images have to be imported from the assetstore via the standard Girder import process. A json specification to filter the import can be added. A common filter is to only import Slide Modality images via { "ModalitiesInStudy": "SM" }.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/formats.html b/formats.html new file mode 100644 index 000000000..725f90dd5 --- /dev/null +++ b/formats.html @@ -0,0 +1,536 @@ + + + + + + + Image Formats — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Image Formats🔗

+
+

Preferred Extensions and Mime Types🔗

+

Images can generally be read regardless of their name. By default, when opening an image with large_image.open(), each tile source reader is tried in turn until one source can open the file. Each source lists preferred file extensions and mime types with a priority level. If the file ends with one of these extensions or has one of these mimetypes, the order that the source readers are tried is adjusted based on the specified priority.

+

The file extensions and mime types that are listed by the core sources that can affect source processing order are listed below. See large_image.listSources() for details about priority of the different source and the large_image.constants.SourcePriority for the priority meaning.

+
+

Extensions🔗

+
1sc
+2fl
+ace2
+acff
+afi
+afm
+aim
+al3d
+ali
+am
+amiramesh
+apl
+apng
+arf
+asc
+avi
+avif
+avs
+b
+bag
+bif
+bil
+bin
+bip
+blp
+blx
+bmp
+bt
+btf
+bufr
+bw
+byn
+c01
+cal
+cfg
+ch5
+cif
+cr2
+crw
+csv
+ct1
+cub
+cur
+cxd
+czi
+dat
+db
+dcm
+dcx
+ddf
+dds
+dem
+dib
+dic
+dicom
+dm2
+dm3
+dm4
+dt0
+dt1
+dt2
+dti
+dv
+dwg
+dz
+dzc
+dzi
+eer
+emf
+eps
+epsi
+err
+ers
+ets
+exp
+exr
+fdf
+fff
+ffr
+fit
+fits
+flc
+flex
+fli
+frm
+ftc
+fts
+ftu
+gbr
+gdb
+gel
+gen
+geotiff
+gff
+gif
+gpkg
+gpkg.zip
+grb
+grb2
+grc
+grd
+grey
+grib
+grib2
+gsb
+gta
+gti
+gti.fgb
+gti.gpkg
+gtx
+gvb
+gxf
+h5
+hdf
+hdf5
+hdr
+hdr.gz
+hed
+heic
+heics
+heif
+heifs
+hf2
+hgt
+hif
+his
+htd
+html
+hx
+i2i
+icb
+icns
+ico
+ics
+ids
+iim
+im
+im3
+img
+img.gz
+ims
+ini
+inr
+ipl
+ipm
+ipw
+isg
+j2c
+j2k
+jfif
+jls
+jp2
+jpc
+jpe
+jpeg
+jpf
+jpg
+jpk
+jpls
+jpt
+jpx
+json
+jxl
+kap
+kea
+klb
+kml
+kmz
+kro
+l2d
+labels
+lbl
+lcp
+lei
+lif
+liff
+lim
+lms
+lsm
+map
+mat
+mbtiles
+mdb
+mea
+mem
+mnc
+mng
+mod
+mov
+mpeg
+mpg
+mpl
+mpo
+mpr
+mrc
+mrcs
+mrf
+mrw
+mrxs
+msp
+msr
+mtb
+mvd2
+n1
+naf
+nat
+nc
+nd
+nd2
+ndpi
+ndpis
+nef
+nhdr
+nia
+nia.gz
+nii
+nii.gz
+nitf
+nrrd
+ntf
+obf
+obsep
+oib
+oif
+oir
+ome
+ome.btf
+ome.tf2
+ome.tf8
+ome.tif
+ome.tiff
+ome.xml
+palm
+par
+pbm
+pcd
+pcoraw
+pcx
+pdf
+pds
+pfm
+pgm
+pic
+pict
+pix
+png
+pnl
+pnm
+ppi
+ppm
+pr3
+prf
+ps
+psd
+ptif
+ptiff
+pxr
+qoi
+qpi
+qptiff
+r3d
+ras
+raw
+rcpnl
+rda
+rec
+res
+rgb
+rgba
+rik
+rst
+rsw
+scn
+sdat
+sdt
+seq
+sg-grd-z
+sgi
+sid
+sif
+sigdem
+sld
+sm2
+sm3
+spc
+spe
+spi
+sqlite
+st
+stk
+stp
+svs
+svslide
+sxm
+szi
+tc
+ter
+tf2
+tf8
+tfr
+tga
+tif
+tiff
+tnb
+toc
+top
+tpkx
+txt
+v
+vda
+vff
+vips
+vms
+vmu
+vrt
+vsi
+vst
+vws
+wat
+webp
+wlz
+wmf
+wpi
+xbm
+xdce
+xml
+xpm
+xqd
+xqf
+xv
+xys
+xyz
+yaml
+yml
+zarr
+zarray
+zattrs
+zfp
+zfr
+zgroup
+zif
+zip
+zvi
+
+343 extensions
+
+
+
+
+

Mime Types🔗

+
application/dicom
+application/json
+application/pdf
+application/postscript
+application/vnd+zarr
+application/x-zarr
+application/yaml
+application/zip+zarr
+image/avif
+image/bmp
+image/czi
+image/geotiff
+image/gif
+image/heic
+image/heif
+image/icns
+image/jls
+image/jp2
+image/jpeg
+image/jpx
+image/jxl
+image/mirax
+image/mpo
+image/nd2
+image/palm
+image/png
+image/rgb
+image/scn
+image/sgi
+image/tiff
+image/vnd.adobe.photoshop
+image/vsi
+image/webp
+image/x-icon
+image/x-pcx
+image/x-portable-anymap
+image/x-ptif
+image/x-tga
+image/x-tiff
+image/x-xpixmap
+image/xbm
+image/xpm
+video/mpeg
+
+43 mime types
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/genindex.html b/genindex.html new file mode 100644 index 000000000..498616cef --- /dev/null +++ b/genindex.html @@ -0,0 +1,3502 @@ + + + + + + Index — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ A + | B + | C + | D + | E + | F + | G + | H + | I + | J + | K + | L + | M + | N + | O + | P + | R + | S + | T + | U + | V + | W + | Y + | Z + +
+

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

H

+ + + +
+ +

I

+ + + +
+ +

J

+ + + +
+ +

K

+ + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

N

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

W

+ + + +
+ +

Y

+ + + +
+ +

Z

+ + + +
+ + + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/getting_started.html b/getting_started.html new file mode 100644 index 000000000..4ad77028d --- /dev/null +++ b/getting_started.html @@ -0,0 +1,482 @@ + + + + + + + Getting Started — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Getting Started🔗

+

The large_image library can be used to read and access different file formats. There are several common usage patterns. +To read more about accepted file formats, visit the Image Formats page.

+

These examples use sample.tiff as an example – any readable image can be used in this case. Visit demo.kitware.com to download a sample image.

+
+

Installation🔗

+

In addition to installing the base large-image package, you’ll need at least one tile source which corresponds to your target file format(s) (a large-image-source-xxx package). You can install everything from the main project with one of these commands:

+
+

Pip🔗

+

Install common tile sources on linux, OSX, or Windows:

+
pip install large-image[common]
+
+
+

Install all tile sources on linux:

+
pip install large-image[all] --find-links https://girder.github.io/large_image_wheels
+
+
+

When using large-image with an instance of Girder, install all tile sources and all Girder plugins on linux:

+
pip install large-image[all] girder-large-image-annotation[tasks] --find-links https://girder.github.io/large_image_wheels
+
+
+
+
+

Conda🔗

+

Conda makes dependency management a bit easier if not on Linux. The base module, converter module, and two of the source modules are available on conda-forge. You can install the following:

+
conda install -c conda-forge large-image
+conda install -c conda-forge large-image-source-gdal
+conda install -c conda-forge large-image-source-tiff
+conda install -c conda-forge large-image-converter
+
+
+
+
+
+

Reading Image Metadata🔗

+

All images have metadata that include the base image size, the base tile size, the number of conceptual levels, and information about the size of a pixel in the image if it is known.

+
import large_image
+source = large_image.open('sample.tiff')
+print(source.getMetadata())
+
+
+

This might print a result like:

+
{
+    'levels': 9,
+    'sizeX': 58368,
+    'sizeY': 12288,
+    'tileWidth': 256,
+    'tileHeight': 256,
+    'magnification': 40.0,
+    'mm_x': 0.00025,
+    'mm_y': 0.00025
+}
+
+
+

levels doesn’t actually tell which resolutions are present in the file. It is the number of levels that can be requested from the getTile method. The levels can also be computed via ceil(log(max(sizeX / tileWidth, sizeY / tileHeight)) / log(2)) + 1.

+

The mm_x and mm_y values are the size of a pixel in millimeters. These can be None if the value is unknown. The magnification is that reported by the file itself, and may be None. The magnification can be approximated by 0.01 / mm_x.

+
+
+

Getting a Region of an Image🔗

+

You can get a portion of an image at different resolutions and in different formats. Internally, the large_image library reads the minimum amount of the file necessary to return the requested data, caching partial results in many instances so that a subsequent query may be faster.

+
import large_image
+source = large_image.open('sample.tiff')
+image, mime_type = source.getRegion(
+    region=dict(left=1000, top=500, right=11000, bottom=1500),
+    output=dict(maxWidth=1000),
+    encoding='PNG')
+# image is a PNG that is 1000 x 100.  Specifically, it will be a bytes
+# object that represent a PNG encoded image.
+
+
+

You could also get this as a numpy array:

+
import large_image
+source = large_image.open('sample.tiff')
+nparray, mime_type = source.getRegion(
+    region=dict(left=1000, top=500, right=11000, bottom=1500),
+    output=dict(maxWidth=1000),
+    format=large_image.constants.TILE_FORMAT_NUMPY)
+# Our source image happens to be RGB, so nparray is a numpy array of shape
+# (100, 1000, 3)
+
+
+

You can specify the size in physical coordinates:

+
import large_image
+source = large_image.open('sample.tiff')
+nparray, mime_type = source.getRegion(
+    region=dict(left=0.25, top=0.125, right=2.75, bottom=0.375, units='mm'),
+    scale=dict(mm_x=0.0025),
+    format=large_image.constants.TILE_FORMAT_NUMPY)
+# Since our source image had mm_x = 0.00025 for its scale, this has the
+# same result as the previous example.
+
+
+
+
+

Tile Serving🔗

+

One of the uses of large_image is to get tiles that can be used in image or map viewers. Most of these viewers expect tiles that are a fixed size and known resolution. The getTile method returns tiles as stored in the original image and the original tile size. If there are missing levels, these are synthesized – this is only done for missing powers-of-two levels or missing tiles. For instance,

+
import large_image
+source = large_image.open('sample.tiff')
+# getTile takes x, y, z, where x and y are the tile location within the
+# level and z is level where 0 is the lowest resolution.
+tile0 = source.getTile(0, 0, 0)
+# tile0 is the lowest resolution tile that shows the whole image.  It will
+# be a JPEG or PNG or some other image format depending on the source
+tile002 = source.getTile(0, 0, 2)
+# tile002 will be a tile representing no more than 1/4 the width of the
+# image in the upper-left corner.  Since the z (third parameter) is 2, the
+# level will have up to 2**2 x 2**2 (4 x 4) tiles.  An image doesn't
+# necessarily have all tiles in that range, as the image may not be square.
+
+
+

Some methods such as getRegion and getThumbnail allow you to specify format on the fly. But note that since tiles need to be cached in a consistent format, getTile always returns the same format depending on what encoding was specified when it was opened:

+
import large_image
+source = large_image.open('sample.tiff', encoding='PNG')
+tile0 = source.getTile(0, 0, 0)
+# tile is now guaranteed to be a PNG
+
+
+

Tiles are always tileWidth by tileHeight in pixels. At the maximum level (z = levels - 1), the number of tiles in that level will range in x from 0 to strictly less than sizeX / tileWidth, and y from 0 to strictly less than sizeY / tileHeight. For each lower level, the is a power of two less tiles. For instance, when z = levels - 2, x ranges from 0 to less than sizeX / tileWidth / 2; at z = levels - 3, x is less than sizeX / tileWidth / 4.

+
+
+

Iterating Across an Image🔗

+

Since most images are too large to conveniently fit in memory, it is useful to iterate through the image. +The tileIterator function can take the same parameters as getRegion to pick an output size and scale, but can also specify a tile size and overlap. +You can also get a specific tile with those parameters. This tiling doesn’t have to have any correspondence to the tiling of the original file. +The data for each tile is loaded lazily, only once tile['tile'] or tile['format'] is accessed.

+
import large_image
+source = large_image.open('sample.tiff')
+for tile in source.tileIterator(
+    tile_size=dict(width=512, height=512),
+    format=large_image.constants.TILE_FORMAT_NUMPY
+):
+    # tile is a dictionary of information about the specific tile
+    # tile['tile'] contains the actual numpy or image data
+    print(tile['x'], tile['y'], tile['tile'].shape)
+    # This will print something like:
+    #   0 0 (512, 512, 3)
+    #   512 0 (512, 512, 3)
+    #   1024 0 (512, 512, 3)
+    #   ...
+    #   56832 11776 (512, 512, 3)
+    #   57344 11776 (512, 512, 3)
+    #   57856 11776 (512, 512, 3)
+
+
+

You can overlap tiles. For instance, if you are running an algorithm where there are edge effects, you probably want an overlap that is big enough that you can trim off or ignore those effects:

+
import large_image
+source = large_image.open('sample.tiff')
+for tile in source.tileIterator(
+    tile_size=dict(width=2048, height=2048),
+    tile_overlap=dict(x=128, y=128, edges=False),
+    format=large_image.constants.TILE_FORMAT_NUMPY
+):
+    print(tile['x'], tile['y'], tile['tile'].shape)
+    # This will print something like:
+    #   0 0 (2048, 2048, 3)
+    #   1920 0 (2048, 2048, 3)
+    #   3840 0 (2048, 2048, 3)
+    #   ...
+    #   53760 11520 (768, 2048, 3)
+    #   55680 11520 (768, 2048, 3)
+    #   57600 11520 (768, 768, 3)
+
+
+
+
+

Getting a Thumbnail🔗

+

You can get a thumbnail of an image in different formats or resolutions. The default is typically JPEG and no larger than 256 x 256. Getting a thumbnail is essentially the same as doing getRegion, except that it always uses the entire image and has a maximum width and/or height.

+
import large_image
+source = large_image.open('sample.tiff')
+image, mime_type = source.getThumbnail()
+open('thumb.jpg', 'wb').write(image)
+
+
+

You can get the thumbnail in other image formats and sizes:

+
import large_image
+source = large_image.open('sample.tiff')
+image, mime_type = source.getThumbnail(width=640, height=480, encoding='PNG')
+open('thumb.png', 'wb').write(image)
+
+
+
+
+

Associated Images🔗

+

Many digital pathology images (also called whole slide images or WSI) contain secondary images that have additional information. This commonly includes label and macro images. A label image is a separate image of just the label of a slide. A macro image is a small image of the entire slide either including or excluding the label. There can be other associated images, too.

+
import large_image
+source = large_image.open('sample.tiff')
+print(source.getAssociatedImagesList())
+# This prints something like:
+#   ['label', 'macro']
+image, mime_type = source.getAssociatedImage('macro')
+# image is a binary image, such as a JPEG
+image, mime_type = source.getAssociatedImage('macro', encoding='PNG')
+# image is now a PNG
+image, mime_type = source.getAssociatedImage('macro', format=large_image.constants.TILE_FORMAT_NUMPY)
+# image is now a numpy array
+
+
+

You can get associated images in different encodings and formats. The entire image is always returned.

+
+
+

Projections🔗

+

large_image handles geospatial images. These can be handled as any other image in pixel-space by just opening them normally. Alternately, these can be opened with a new projection and then referenced using that projection.

+
import large_image
+# Open in Web Mercator projection
+source = large_image.open('sample.geo.tiff', projection='EPSG:3857')
+print(source.getMetadata()['bounds'])
+# This will have the corners in Web Mercator meters, the projection, and
+# the minimum and maximum ranges.
+#   We could also have done
+print(source.getBounds())
+# The 0, 0, 0 tile is now the whole world excepting the poles
+tile0 = source.getTile(0, 0, 0)
+
+
+
+
+

Images with Multiple Frames🔗

+

Some images have multiple “frames”. Conceptually, these are images that could have multiple channels as separate images, such as those from fluorescence microscopy, multiple “z” values from serial sectioning of thick tissue or adjustment of focal plane in a microscope, multiple time (“t”) values, or multiple regions of interest (frequently referred as “xy”, “p”, or “v” values).

+

Any of the frames of such an image are accessed by adding a frame=<integer> parameter to the getTile, getRegion, tileIterator, or other methods.

+
import large_image
+source = large_image.open('sample.ome.tiff')
+print(source.getMetadata())
+# This will print something like
+#   {
+#     'magnification': 8.130081300813009,
+#     'mm_x': 0.00123,
+#     'mm_y': 0.00123,
+#     'sizeX': 2106,
+#     'sizeY': 2016,
+#     'tileHeight': 1024,
+#     'tileWidth': 1024,
+#     'IndexRange': {'IndexC': 3},
+#     'IndexStride': {'IndexC': 1},
+#     'frames': [
+#       {'Frame': 0, 'Index': 0, 'IndexC': 0, 'IndexT': 0, 'IndexZ': 0},
+#       {'Frame': 1, 'Index': 0, 'IndexC': 1, 'IndexT': 0, 'IndexZ': 0},
+#       {'Frame': 2, 'Index': 0, 'IndexC': 2, 'IndexT': 0, 'IndexZ': 0}
+#     ]
+#   }
+nparray, mime_type = source.getRegion(
+    frame=1,
+    format=large_image.constants.TILE_FORMAT_NUMPY)
+# nparray will contain data from the middle channel image
+
+
+
+
+

Channels, Bands, Samples, and Axes🔗

+

Various large image formats refer to channels, bands, and samples. This isn’t consistent across different libraries. In an attempt to harmonize the geospatial and medical image terminology, large_image uses bands or samples to refer to image plane components, such as red, green, blue, and alpha. For geospatial data this can often have additional bands, such as near infrared or panchromatic. channels are stored as separate frames and can be interpreted as different imaging modalities. For example, a fluorescence microscopy image might have DAPI, CY5, and A594 channels. A common color photograph file has 3 bands (also called samples) and 1 channel.

+

At times, image axes are used to indicate the order of data, especially when interpreted as an n-dimensional array. The x and y axes are the horizontal and vertical dimensions of the image. The s axis is the bands or samples, such as red, green, and blue. The c axis is the channels with special support for channel names. This corresponds to distinct frames.

+

The z and t are common enough that they are sometimes considered as primary axes. z corresponds to the direction orthogonal to x and y and is usually associated with altitude or microscope stage height. t is time.

+

Other axes are supported provided their names are case-insensitively unique.

+
+
+

Styles - Changing colors, scales, and other properties🔗

+

By default, reading from an image gets the values stored in the image file. If you get a JPEG or PNG as the output, the values will be 8-bit per channel. If you get values as a numpy array, they will have their original resolution. Depending on the source image, this could be 16-bit per channel, floats, or other data types.

+

Especially when working with high bit-depth images, it can be useful to modify the output. For example, you can adjust the color range:

+
import large_image
+source = large_image.open('sample.tiff', style={'min': 'min', 'max': 'max'})
+# now, any calls to getRegion, getTile, tileIterator, etc. will adjust the
+# intensity so that the lowest value is mapped to black and the brightest
+# value is mapped to white.
+image, mime_type = source.getRegion(
+    region=dict(left=1000, top=500, right=11000, bottom=1500),
+    output=dict(maxWidth=1000))
+# image will use the full dynamic range
+
+
+

You can also composite a multi-frame image into a false-color output:

+
import large_image
+source = large_image.open('sample.tiff', style={'bands': [
+    {'frame': 0, 'min': 'min', 'max': 'max', 'palette': '#f00'},
+    {'frame': 3, 'min': 'min', 'max': 'max', 'palette': '#0f0'},
+    {'frame': 4, 'min': 'min', 'max': 'max', 'palette': '#00f'},
+]})
+# Composite frames 0, 3, and 4 to red, green, and blue channels.
+image, mime_type = source.getRegion(
+    region=dict(left=1000, top=500, right=11000, bottom=1500),
+    output=dict(maxWidth=1000))
+# image is false-color and full dynamic range of specific frames
+
+
+
+
+

Writing an Image🔗

+

If you wish to visualize numpy data, large_image can write a tiled tiff. This requires a tile source that supports writing to be installed. As of this writing, the large-image-source-zarr and large-image-source-vips sources supports this. If both are installed, the large-image-source-zarr is the default.

+
import large_image
+source = large_image.new()
+for nparray, x, y in fancy_algorithm():
+    # We could optionally add a mask to limit the output
+    source.addTile(nparray, x, y)
+source.write('/tmp/sample.tiff', lossy=False)
+
+
+

The large-image-source-zarr can be used to store multiple frame data with arbitrary axes.

+
import large_image
+source = large_image.new()
+for nparray, x, y, time, param1 in fancy_algorithm():
+    source.addTile(nparray, x, y, time=time, p1=param1)
+# The writer supports a variety of formats
+source.write('/tmp/sample.zarr.zip', lossy=False)
+
+
+

You may also choose to read tiles from one source and write modified tiles to a new source:

+
import large_image
+original_source = large_image.open('path/to/original/image.tiff')
+new_source = large_image.new()
+for frame in original_source.getMetadata().get('frames', []):
+    for tile in original_source.tileIterator(frame=frame['Frame'], format='numpy'):
+        t, x, y = tile['tile'], tile['x'], tile['y']
+        kwargs = {
+            'z': frame['IndexZ'],
+            'c': frame['IndexC'],
+        }
+        modified_tile = modify_tile(t)
+        new_source.addTile(modified_tile, x=x, y=y, **kwargs)
+new_source.write('path/to/new/image.tiff', lossy=False)
+
+
+

In some cases, it may be beneficial to write to a single image from multiple processes or threads:

+
import large_image
+import multiprocessing
+# Important: Must be a pickleable function
+def add_tile_to_source(tilesource, nparray, position):
+    tilesource.addTile(
+        nparray,
+        **position
+    )
+source = large_image.new()
+# Important: Maximum size must be allocated before any multiprocess concurrency
+add_tile_to_source(source, np.zeros(1, 1, 3), dict(x=max_x, y=max_y, z=max_z))
+# Also works with multiprocessing.ThreadPool, which does not need maximum size allocated first
+with multiprocessing.Pool(max_workers=5) as pool:
+    pool.starmap(
+        add_tile_to_source,
+        [(source, t, t_pos) for t, t_pos in tileset]
+    )
+source.write('/tmp/sample.zarr.zip', lossy=False)
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/girder_annotation_config_options.html b/girder_annotation_config_options.html new file mode 100644 index 000000000..b5bf0912f --- /dev/null +++ b/girder_annotation_config_options.html @@ -0,0 +1,204 @@ + + + + + + + Girder Annotation Configuration Options — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Girder Annotation Configuration Options🔗

+

The large_image annotation plugin adds models to the Girder database for supporting annotating large images. These annotations can be rendered on images. +Annotations can include polygons, points, image overlays, and other types (see Annotation Schema). Each annotation can have a label and metadata. +Additional user interface libraries allow other libraries (like HistomicsUI) to let a user interactively add and modify annotations.

+
+

General Plugin Settings🔗

+

There are some general plugin settings that affect large_image annotation as a Girder plugin. These settings can be accessed by an Admin user through the Admin Console / Plugins and selecting the gear icon next to Large image annotation.

+
+

Store annotation history🔗

+

If Record annotation history is selected, whenever annotations are saved, previous versions are kept in the database. This can greatly increase the size of the database. The old versions of the annotations allow the API to be used to revent to previous versions or to audit changes over time.

+
+
+

.large_image_config.yaml🔗

+

This can be used to specify how annotations are listed on the item page.

+
---
+# If present, show a table with column headers in annotation lists
+annotationList:
+  # show these columns in order from left to right.  Each column has a
+  # "type" and "value".  It optionally has a "title" used for the column
+  # header, and a "format" used for searching and filtering.  There are
+  # always control columns at the left and right.
+  columns:
+    -
+      # The "record" type is from the default annotation record.  The value
+      # is one of "name", "creator", "created", "updatedId", "updated",
+      type: record
+      value: name
+    -
+      type: record
+      value: creator
+      # A format of user will print the user name instead of the id
+      format: user
+    -
+      type: record
+      value: created
+      # A format of date will use the browser's default date format
+      format: date
+    -
+      # The "metadata" type is taken from the annotations's
+      # "annotation.attributes" contents.  It can be a nested key by using
+      # dots in its name.
+      type: metadata
+      value: Stain
+      # "format" can be "text", "number", "category".  Other values may be
+      # specified later.
+      format: text
+  defaultSort:
+    # The default lists a sort order for sortable columns.  This must have
+    # type, value, and dir for each entry, where dir is either "up" or
+    # "down".
+    -
+      type: metadata
+      value: Stain
+      dir: up
+    -
+      type: record
+      value: name
+      dir: down
+
+
+

These values can be combined with values from the base large_image plugin.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/girder_caching.html b/girder_caching.html new file mode 100644 index 000000000..582802382 --- /dev/null +++ b/girder_caching.html @@ -0,0 +1,195 @@ + + + + + + + Caching Large Image in Girder — large_image documentation + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Caching Large Image in Girder🔗

+

Tile sources are opened: when large image files uploaded or imported; when large image files are viewed; when thumbnails are generated; when an item page with a large image is viewd; and from some API calls. All of these result in the source being placed in the cache _except_ import.

+

Since there are multiple users, the cache size should be large enough that no user has an image that they are actively viewing fall out of cache.

+

Example of cache use when the GET /item/{id}/tile/zxy/{z}/{x}/{y}?style=<style>&encoding=<encoding>&... endpoint is called:

+
+ graph TD; + apicall(["GET /item/{id}/tile/zxy/{z}/{x}/{y}?style=&lt;style&gt;&amp;encoding=&lt;encoding&gt;&amp;..."]) + checktile1{Is tile in\ntile cache?} + apicall --> checktile1; + getfile[("Get file-like object\nor url from Girder")] + checktile1 -- No --> getfile; + servetile(["Serve tile 🖼️"]) + checktile1 -- Yes --> servetile; + checksource{"Is file-like object\nin source cache\nwith {style}\nand {encoding}?"} + getfile --> checksource; + checkunstyled{"Is unstyled file\nin source cache\nwith {encoding}?"} + checksource -- No --> checkunstyled; + openunstyle["Open unstyled file with {encoding}"] + checkunstyled -- No --> openunstyle; + putunstyledsource[/"Put unstyled source in cache"/] + openunstyle --> putunstyledsource; + alter["Copy unstyled source\nand alter for {style}"] + putunstyledsource --> alter; + checkunstyled -- Yes --> alter; + putsource[/"Put styled source in cache"/] + alter --> putsource; + checktile2{"Is unstyled tile\n{x}, {y}, {z},\n{frame}, {encoding}\nin tile cache?"} + putsource --> checktile2; + checksource -- Yes --> checktile2; + checktile3{"Does tile exist\nin the file?"} + checktile2 -- No --> checktile3; + maketile[["Synthesize tile\nfrom higher\nresolution tiles"]] + checktile3 -- No --> maketile; + readfile[("Read tile from file")] + checktile3 -- Yes --> readfile; + puttile1[/"Put unstyled\ntile in cache"/] + maketile --> puttile1; + readfile --> puttile1; + gettile[\"Read unstyled tile\nfrom cache"\] + checktile2 -- Yes --> gettile; + needstyle{"Is {style}\nrequired?"} + puttile1 --> needstyle; + gettile --> needstyle; + checkencoding{"Is tile\nin {encoding}?"} + needstyle -- No --> checkencoding; + isnumpy{"Is unstyled\ntile a\nnumpy array?"} + needstyle -- Yes --> isnumpy; + makenumpy["Convert tile to numpy"] + isnumpy -- No --> makenumpy; + makestyle["Generate styled tile in numpy format\n(Note that this could fetch other tiles)"] + isnumpy -- Yes --> makestyle; + makenumpy --> makestyle; + makestyle --> checkencoding; + encode["Encode tile to {encoding}"] + checkencoding -- No --> encode; + puttile2[/"Put encoded tile\nin cache"/] + encode --> puttile2; + checkencoding -- Yes --> puttile2; + puttile2 --> servetile; +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/girder_config_options.html b/girder_config_options.html new file mode 100644 index 000000000..f907b79d4 --- /dev/null +++ b/girder_config_options.html @@ -0,0 +1,532 @@ + + + + + + + Girder Configuration Options — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Girder Configuration Options🔗

+
+

General Plugin Settings🔗

+

There are some general plugin settings that affect large_image as a Girder plugin. These settings can be accessed by an Admin user through the Admin Console / Plugins and selecting the gear icon next to Large image.

+
+
+

YAML Configuration Files🔗

+

Some settings can be specified per-folder tree using yaml files. For these settings, if the configuration file exists in the current folder it is used. If not, the parent folders are checked iteratively up to the parent collection or user. If no configuration file is found, the .config folder in the collection or user is checked for the file. Lastly, the Configuration Folder specified on the plugin settings page is checked for the configuration file.

+

The configuration files can have different configurations based on the user’s access level and group membership.

+

The yaml file has the following structure:

+
---
+# most settings are key-value pairs, where the value could be another
+# dictionary with keys and values, lists, or other valid data.
+<key>: <value>
+# The access key is special
+access:
+  # logged in users get these settings
+  user:
+    # If the value is a dictionary and the key matches a key at the base
+    # level, then the values are combined.  To completely replace the base
+    # value, add the special key "__all__" and set it's value to true.
+    <key>: <value>
+  # admin users get these settings
+  admin:
+    <key>: <value>
+# The groups key specifies that specific user groups have distinct settings
+groups:
+  <group name>:
+    <key>: <value>
+    # groups can specify access based on user or admin, too.
+    access: ...
+# If __inherit__ is true, then merge this config file with the next config
+# file in the parent folder hierarchy.
+__inherit__: true
+
+
+
+

.large_image_config.yaml🔗

+
+

Item Lists🔗

+

This is used to specify how items appear in item lists. There are two settings, one for folders in the main Girder UI and one for folders in dialogs (such as when browsing in the file dialog).

+
---
+# If present, show a table with column headers in item lists
+itemList:
+  # layout does not need to be specified.
+  layout:
+    # The default layout is a list.  This can optionally be "grid"
+    mode: grid
+    # max-width is only used in grid mode.  It is the maximum width in
+    # pixels for grid entries.  It defaults to 250.
+    max-width: 250
+  # show these columns in order from left to right.  Each column has a
+  # "type" and "value".  It optionally has a "title" used for the column
+  # header, and a "format" used for searching and filtering.  The "label",
+  # if any, is displayed to the left of the column value.  This is more
+  # useful in an grid view than in a column view.
+  columns:
+    -
+      # The "image" type's value is either "thumbnail" or the name of an
+      # associated image, such as "macro" or "label".
+      type: image
+      value: thumbnail
+      title: Thumbnail
+      # The maximum size of images can be specified.  It defaults to 160 x
+      # 100.  It will always maintain the original aspect ratio.
+      width: 250
+      height: 250
+    -
+      type: image
+      value: label
+      title: Slide Label
+    -
+      # The "record" type is from the default item record.  The value is
+      # one of "name", "size", or "controls".
+      type: record
+      value: name
+    -
+      type: record
+      value: size
+    -
+      type: record
+      value: controls
+    -
+      # The "metadata" type is taken from the item's "meta" contents.  It
+      # can be a nested key by using dots in its name.
+      type: metadata
+      value: Stain
+      # "format" can be "text", "number", "category".  Other values may be
+      # specified later.
+      format: text
+    -
+      type: metadata
+      # This will get "Label" from the first entry in array "gloms"
+      value: gloms.0.Label
+      title: First Glom Label
+    -
+      type: metadata
+      # You can use some javascript-like properties, such as .length for
+      # the length of arrays.
+      value: gloms.length
+      title: Number of Gloms
+      # You can have this value be populated for just some of the items by
+      # specifying an "only" list.  Each entry in the only list must have
+      # the "type" and "value" as per the column it is filtering on, plus a
+      # "match" value that is used as a case-insensitive RegExp.  All such
+      # limits must match to show the value.
+      only:
+        -
+          type: record
+          value: name
+          # only show this for items whose names end with ".svs".
+          match: "\\.svs$"
+    # You can edit metadata in a item list by adding the edit: true entry
+    # and the options from the itemMetadata records that are detailed
+    # below.  In this case, edits to metadata that validate are saved
+    # immediately.
+    -
+      type: metadata
+      value: userstain
+      title: User Stain
+      label: User Stain
+      edit: true
+      # description is used as both a tooltip and as placeholder text
+      description: Staining method
+      # if required is true, the value can't be empty
+      required: true
+      # If a regex is specified, the value must match
+      # regex: '^(Eosin|H&E|Other)$'
+      # If an enum is specified, the value is set via a dropdown select box
+      enum:
+        - Eosin
+        - H&E
+        - Other
+      # If a default is specified, if the value is unset, it will show this
+      # value in the control
+      default: H&E
+  defaultSort:
+    # The default lists a sort order for sortable columns.  This must have
+    # type, value, and dir for each entry, where dir is either "up" or
+    # "down".
+    -
+      type: metadata
+      value: Stain
+      dir: up
+    -
+      type: record
+      value: name
+      dir: down
+itemListDialog:
+  # Show these columns
+  columns:
+    -
+      type: image
+      value: thumbnail
+      title: Thumbnail
+    -
+      type: record
+      value: name
+    -
+      type: metadata
+      value: Stain
+      format: text
+    -
+      type: record
+      value: size
+
+
+

If there are no large images in a folder, none of the image columns will appear.

+
+
+

Item Metadata🔗

+

By default, item metadata can contain any keys and values. These can be given better titles and restricted in their data types.

+
---
+# If present, offer to add these specific keys and restrict their datatypes
+itemMetadata:
+  -
+    # value is the key name within the metadata
+    value: stain
+    # title is the displayed titles
+    title: Stain
+    # description is used as both a tooltip and as placeholder text
+    description: Staining method
+    # if required is true, the delete button does not appear
+    required: true
+    # If a regex is specified, the value must match
+    # regex: '^(Eosin|H&E|Other)$'
+    # If an enum is specified, the value is set via a dropdown select box
+    enum:
+      - Eosin
+      - H&E
+      - Other
+    # If a default is specified, when the value is created, it will show
+    # this value in the control
+    default: H&E
+  -
+    value: rating
+    # type can be "number", "integer", or "text" (default)
+    type: number
+    # minimum and maximum are inclusive
+    minimum: 0
+    maximum: 10
+    # Exclusive values can be specified instead
+    # exclusiveMinimum: 0
+    # exclusiveMaximum: 10
+
+
+
+
+

Image Frame Presets🔗

+

This is used to specify a list of presets for viewing images in the folder. +Presets can be customized and saved in the GeoJS Image Viewer. +To retrieve saved presets, use [serverURL]/api/v1/item/[itemID]/internal_metadata/presets. +You can convert the response to YAML and paste it into the imageFramePresets key in your config file.

+

Each preset can specify a name, a view mode, an image frame, and style options.

+
    +
  • The name of a preset can be any string which uniquely identifies the preset.

  • +
  • There are four options for mode:

    +
      +
    • Frame control

      +
        +
      • id: 0

      • +
      • name: Frame

      • +
      +
    • +
    • Axis control

      +
        +
      • id: 1

      • +
      • name: Axis

      • +
      +
    • +
    • Channel Compositing

      +
        +
      • id: 2

      • +
      • name: Channel Compositing

      • +
      +
    • +
    • Band Compositing

      +
        +
      • id: 3

      • +
      • name: Band Compositing

      • +
      +
    • +
    +
  • +
  • The frame of a preset is a 0-based index representing a single frame in a multiframe image. +For single-frame images, this value will always be 0. +For channel compositing, each channel will have a framedelta value which represents distance from this base frame value. +The result of channel compositing is multiple frames (calculated via framedelta) composited together.

  • +
  • The style of a preset is a dictionary with a schema similar to the [style schema for tile retrieval](tilesource_options.rst#style). The value for a preset’s style consists of a band definition, where each band may have the following:

    +
      +
    • band: A 1-based index of a band within the current frame

    • +
    • framedelta: An integer representing distance from the current frame, used for compositing multiple frames together

    • +
    • palette: A hexadecimal string beginning with “#” representing a color to stain this frame

    • +
    • min: The value to map to the first palette value

    • +
    • max: The value to map to the last palette value

    • +
    • autoRange: A shortcut for excluding a percentage from each end of the value distribution in the image. Express as a float.

    • +
    +
  • +
+

The YAML below includes some example presets.

+
---
+# If present, each preset in this list will be added to the preset list
+# of every image in the folder for which the preset is applicable
+imageFramePresets:
+- name: Frame control - Frame 4
+  frame: 4
+  mode:
+    id: 0
+    name: Frame
+- name: Axis control - Frame 25
+  frame: 25
+  mode:
+    id: 1
+    name: Axis
+- name: 3 channels
+  frame: 0
+  mode:
+    id: 2
+    name: Channel Compositing
+  style:
+    bands:
+    - framedelta: 0
+      palette: "#0000FF"
+    - framedelta: 1
+      palette: "#FF0000"
+    - framedelta: 2
+      palette: "#00FF00"
+- name: 3 bands
+  frame: 0
+  mode:
+    id: 3
+    name: Band Compositing
+  style:
+    bands:
+    - band: 1
+      palette: "#0000FF"
+    - band: 2
+      palette: "#FF0000"
+    - band: 3
+      palette: "#00FF00"
+- name: Channels with Min and Max
+  frame: 0
+  mode:
+    id: 2
+    name: Channel Compositing
+  style:
+    bands:
+    - min: 18000
+      max: 43000
+      framedelta: 0
+      palette: "#0000FF"
+    - min: 18000
+      max: 43000
+      framedelta: 1
+      palette: "#FF0000"
+    - min: 18000
+      max: 43000
+      framedelta: 2
+      palette: "#00FF00"
+    - min: 18000
+      max: 43000
+      framedelta: 3
+      palette: "#FFFF00"
+- name: Auto Ranged Channels
+  frame: 0
+  mode:
+    id: 2
+    name: Channel Compositing
+  style:
+    bands:
+    - autoRange: 0.2
+      framedelta: 0
+      palette: "#0000FF"
+    - autoRange: 0.2
+      framedelta: 1
+      palette: "#FF0000"
+    - autoRange: 0.2
+      framedelta: 2
+      palette: "#00FF00"
+    - autoRange: 0.2
+      framedelta: 3
+      palette: "#FFFF00"
+    - autoRange: 0.2
+      framedelta: 4
+      palette: "#FF00FF"
+    - autoRange: 0.2
+      framedelta: 5
+      palette: "#00FFFF"
+    - autoRange: 0.2
+      framedelta: 6
+      palette: "#FF8000"
+
+
+
+
+

Image Frame Preset Defaults🔗

+

This is used to specify a list of preset defaults, in order of precedence. +These presets are to be automatically applied to an image in this folder if they are applicable. +In the case that a preset is not applicable to an image, the next item in this list will be used.

+

** Important: the presets named in this list must have corresponding entries in the imageFramePresets configuration, else this configuration will have no effect. **

+
---
+# The preset named "Primary Preset" will be applied to all images in this folder.
+# Any images for which "Primary Preset" does not apply will have "Secondary Preset" applied.
+# Any images for which neither "Primary Preset" nor "Secondary Preset" apply will have "Tertiary Preset" applied.
+imageFramePresetDefaults:
+- name: Primary Preset
+- name: Secondary Preset
+- name: Tertiary Preset
+
+
+
---
+# This example would be used with the example for ``imageFramePresets`` shown above.
+# Images with 7 or more channels would use "Auto Ranged Channels"
+# Images with fewer than 7 but at least 4 channels would use "Channels with Min and Max"
+# Images with 3 channels would use "3 channels"
+# Images with fewer than 3 channels would not have a default preset applied.
+imageFramePresetDefaults:
+- name: Auto Ranged Channels
+- name: Channels with Min and Max
+- name: 3 channels
+
+
+
+
+
+
+

Editing Configuration Files🔗

+

Some file types can be edited on their item page. This is detected based on the mime type associated with the file: application/json for json files and text/yaml or text/x-yaml for yaml files. If a user has enough permissions, these can be modified and saved. Note that this does not alter imported files; rather, on save it will create a new file in the assetstore and use that; this works fine for using the configuration files.

+

For admins, there is also support for the application/x-girder-ini mime type for Girder configuration files. This has a special option to replace the existing Girder configuration and restart the server and should be used with due caution.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/girder_index.html b/girder_index.html new file mode 100644 index 000000000..67f2ed6b0 --- /dev/null +++ b/girder_index.html @@ -0,0 +1,149 @@ + + + + + + + Girder Integration — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Girder Integration🔗

+

Girder is a free and open source web-based data management platform developed by Kitware. Girder is both a standalone application and a platform for building new web services. large-image has a Girder plugin that can be added to any Girder instance to manage and view large images.

+

To see an example Girder application with the large-image plugin, visit demo.kitware.com.

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/image_conversion.html b/image_conversion.html new file mode 100644 index 000000000..9e7d05dcf --- /dev/null +++ b/image_conversion.html @@ -0,0 +1,229 @@ + + + + + + + Image Conversion — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Image Conversion🔗

+

The large_image library can read a variety of images with the various tile source modules. Some image files that cannot be read directly can be converted into a format that can be read by the large_image library. Additionally, some images that can be read are very slow to handle because they are stored inefficiently, and converting them will make a equivalent file that is more efficient.

+
+

Python Usage🔗

+

The large_image_converter module can be used as a Python package. See large_image_converter for details.

+
+
+

Command Line Usage🔗

+

Installing the large-image-converter module adds a large_image_converter command to the local environment. Running large_image_converter --help displays the various options.

+
usage: large_image_converter [-h] [--version] [--verbose] [--silent]
+                             [--overwrite] [--tile TILESIZE] [--no-subifds]
+                             [--subifds] [--frame ONLYFRAME]
+                             [--format {tiff,aperio}]
+                             [--compression {,jpeg,deflate,zip,lzw,zstd,packbits,jbig,lzma,webp,jp2k,none}]
+                             [--quality QUALITY] [--level LEVEL]
+                             [--predictor {,none,horizontal,float,yes}]
+                             [--psnr PSNR] [--cr CR]
+                             [--shrink-mode {mean,median,mode,max,min,nearest,default}]
+                             [--only-associated _KEEP_ASSOCIATED]
+                             [--exclude-associated _EXCLUDE_ASSOCIATED]
+                             [--concurrency _CONCURRENCY] [--stats]
+                             [--stats-full]
+                             source [dest]
+
+Convert files for use with Large Image. Output files are written as tiled tiff
+files. For geospatial files, these conform to the cloud-optimized geospatial
+tiff format (COG). For non-geospatial, the output image will be either 8- or
+16-bits per sample per channel. Some compression formats are always 8-bits per
+sample (webp, jpeg), even if that format could support more and the original
+image is higher bit depth.
+
+positional arguments:
+  source                Path to source image
+  dest                  Output path
+
+options:
+  -h, --help            show this help message and exit
+  --version             Report version
+  --verbose, -v         Increase verbosity
+  --silent, -s          Decrease verbosity
+  --overwrite, -w, -y   Overwrite an existing output file
+  --tile TILESIZE, --tile-size TILESIZE, --tilesize TILESIZE, --tileSize TILESIZE, -t TILESIZE
+                        Tile size. Default is 256.
+  --no-subifds          When writing multiframe files, do not use subifds.
+  --subifds             When writing multiframe files, use subifds.
+  --frame ONLYFRAME     When handling a multiframe file, only output a single
+                        frame. This is the zero-based frame number.
+  --format {tiff,aperio}
+                        Output format. The default is a standardized pyramidal
+                        tiff or COG geotiff. Other formats may not be
+                        available for all input options and will change some
+                        defaults. Aperio (svs) defaults to no-subifds. If
+                        there is no label image, a cropped nearly square
+                        thumbnail is used in its place if the source image can
+                        be read by any of the known tile sources.
+  --compression {,jpeg,deflate,zip,lzw,zstd,packbits,jbig,lzma,webp,jp2k,none}, -c {,jpeg,deflate,zip,lzw,zstd,packbits,jbig,lzma,webp,jp2k,none}
+                        Internal compression. Default will use jpeg if the
+                        source appears to be lossy or lzw if lossless. lzw is
+                        the most compatible lossless mode. jpeg is the most
+                        compatible lossy mode. jbig and lzma may not be
+                        available. jp2k will first write the file with no
+                        compression and then rewrite it with jp2k the
+                        specified psnr or compression ratio.
+  --quality QUALITY, -q QUALITY
+                        JPEG or webp compression quality. For webp, specify 0
+                        for lossless. Default is 90.
+  --level LEVEL, -l LEVEL
+                        General compression level. Used for deflate (zip)
+                        (1-9), zstd (1-22), and some others.
+  --predictor {,none,horizontal,float,yes}, -p {,none,horizontal,float,yes}
+                        Predictor for some compressions. Default is horizontal
+                        for non-geospatial data and yes for geospatial.
+  --psnr PSNR           JP2K peak signal to noise ratio. 0 for lossless.
+  --cr CR               JP2K compression ratio. 1 for lossless.
+  --shrink-mode {mean,median,mode,max,min,nearest,default}, --shrink {mean,median,mode,max,min,nearest,default}, --reduce {mean,median,mode,max,min,nearest,default}
+                        When producing lower resolution images, use this
+                        method for computing pixels. This defaults to median
+                        for lossy images and nearest for lossless images.
+  --only-associated _KEEP_ASSOCIATED
+                        Only keep associated images with the specified keys.
+                        The value is used as a matching regex.
+  --exclude-associated _EXCLUDE_ASSOCIATED
+                        Exclude associated images with the specified keys. The
+                        value is used as a matching regex. If a key is
+                        specified for both exclusion and inclusion, it will be
+                        excluded.
+  --concurrency _CONCURRENCY, -j _CONCURRENCY
+                        Maximum processor concurrency. Some conversion tasks
+                        can use multiple processors. A value <= 0 will use the
+                        number of logical processors less that number. This is
+                        a recommendation and is not strict. Default is 0.
+  --stats               Add conversion stats (time and size) to the
+                        ImageDescription of the output file. This involves
+                        writing the file an extra time; the stats do not
+                        include the extra write.
+  --stats-full, --full-stats
+                        Add conversion stats, including noise metrics (PSNR,
+                        etc.) to the output file. This takes more time and
+                        temporary disk space.
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..6babe43ef --- /dev/null +++ b/index.html @@ -0,0 +1,259 @@ + + + + + + + Large Image — large_image documentation + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Large Image🔗

+

Build Status codecov.io License doi-badge pypi-badge

+

Python modules to work with large, multiresolution images.

+

Large Image is developed and maintained by the Data & Analytics group at Kitware, Inc. for processing large geospatial and medical images. This provides the backbone for several of our image analysis platforms including Resonant GeoData, HistomicsUI, and the Digital Slide Archive.

+
+

Highlights🔗

+
    +
  • Tile serving made easy

  • +
  • Supports a wide variety of geospatial and medical image formats

  • +
  • Convert to tiled Cloud Optimized (Geo)Tiffs (also known as pyramidal tiffs)

  • +
  • Python methods for retiling or accessing regions of images efficiently

  • +
  • Options for restyling tiles, such as dynamically applying color and band transform

  • +
+
+
+

Installation🔗

+

In addition to installing the base large-image package, you’ll need at least one tile source which corresponds to your target file format(s) (a large-image-source-xxx package). You can install everything from the main project with one of these commands:

+
+

Pip🔗

+

Install common tile sources on linux, OSX, or Windows:

+
pip install large-image[common]
+
+
+

Install all tile sources on linux:

+
pip install large-image[all] --find-links https://girder.github.io/large_image_wheels
+
+
+

When using large-image with an instance of Girder, install all tile sources and all Girder plugins on linux:

+
pip install large-image[all] girder-large-image-annotation[tasks] --find-links https://girder.github.io/large_image_wheels
+
+
+
+
+

Conda🔗

+

Conda makes dependency management a bit easier if not on Linux. The base module, converter module, and two of the source modules are available on conda-forge. You can install the following:

+
conda install -c conda-forge large-image
+conda install -c conda-forge large-image-source-gdal
+conda install -c conda-forge large-image-source-tiff
+conda install -c conda-forge large-image-converter
+
+
+
+
+

Docker Image🔗

+

Included in this repository’s packages is a pre-built Docker image that has all +of the dependencies to read any supported image format.

+

This is particularly useful if you do not want to install some of the heavier +dependencies like GDAL on your system or want a dedicated and isolated +environment for working with large images.

+

To use, pull the image and run it by mounting a local volume where the +imagery is stored:

+
docker pull ghcr.io/girder/large_image:latest
+docker run -v /path/to/images:/opt/images ghcr.io/girder/large_image:latest
+
+
+
+
+
+

Modules🔗

+

Large Image consists of several Python modules designed to work together. These include:

+
    +
  • large-image: The core module.

    +

    You can specify extras_require of the name of any tile source included with this repository. For instance, you can do pip install large-image[tiff]. There are additional extras_require options:

    +
      +
    • sources: all of the tile sources in the repository, a specific source name (e.g., tiff)

    • +
    • memcached: use memcached for tile caching

    • +
    • converter: include the converter module

    • +
    • colormaps: use matplotlib for named color palettes used in styles

    • +
    • tiledoutput: support for emitting large regions as tiled tiffs

    • +
    • performance: include optional modules that can improve performance

    • +
    • common: all of the tile sources and above packages that will install directly from pypi without other external libraries on linux, OSX, and Windows.

    • +
    • all: for all of the above

    • +
    +
  • +
  • large-image-converter: A utility for using pyvips and other libraries to convert images into pyramidal tiff files that can be read efficiently by large_image. +You can specify extras_require of jp2k to include modules to allow output to JPEG2000 compression, sources to include all sources, stats to include modules to allow computing compression noise statistics, geospatial to include support for converting geospatial sources, or all for all of the optional extras_require.

  • +
  • Tile sources:

    +
      +
    • large-image-source-bioformats: A tile source for reading any file handled by the Java Bioformats library.

    • +
    • large-image-source-deepzoom: A tile source for reading Deepzoom tiles.

    • +
    • large-image-source-dicom: A tile source for reading DICOM Whole Slide Images (WSI).

    • +
    • large-image-source-gdal: A tile source for reading geotiff files via GDAL. This handles source data with more complex transforms than the mapnik tile source.

    • +
    • large-image-source-mapnik: A tile source for reading geotiff and netcdf files via Mapnik and GDAL. This handles more vector issues than the gdal tile source.

    • +
    • large-image-source-multi: A tile source for compositing other tile sources into a single multi-frame source.

    • +
    • large-image-source-nd2: A tile source for reading nd2 (NIS Element) images.

    • +
    • large-image-source-ometiff: A tile source using the tiff library that can handle most multi-frame OMETiff files that are compliant with the specification.

    • +
    • large-image-source-openjpeg: A tile source using the Glymur library to read jp2 (JPEG 2000) files.

    • +
    • large-image-source-openslide: A tile source using the OpenSlide library. This works with svs, ndpi, Mirax, tiff, vms, and other file formats.

    • +
    • large-image-source-pil: A tile source for small images via the Python Imaging Library (Pillow). By default, the maximum size is 4096, but the maximum size can be configured.

    • +
    • large-image-source-tiff: A tile source for reading pyramidal tiff files in common compression formats.

    • +
    • large-image-source-tifffile: A tile source using the tifffile library that can handle a wide variety of tiff-like files.

    • +
    • large-image-source-vips: A tile source for reading any files handled by libvips. This also can be used for writing tiled images from numpy arrays (up to 4 dimensions).

    • +
    • large-image-source-zarr: A tile source using the zarr library that can handle OME-Zarr (OME-NGFF) files as well as some other zarr files. This can also be used for writing N-dimensional tiled images from numpy arrays. Written images can be saved as any supported format.

    • +
    • large-image-source-test: A tile source that generates test tiles, including a simple fractal pattern. Useful for testing extreme zoom levels.

    • +
    • large-image-source-dummy: A tile source that does nothing. This is an absolutely minimal implementation of a tile source used for testing. If you want to create a custom tile source, start with this implementation.

    • +
    +
  • +
+

As a Girder plugin, large-image adds end points to access all of the image formats it can read both to get metadata and to act as a tile server. +In the Girder UI, large-image shows images on item pages, and can show thumbnails in item lists when browsing folders. +There is also cache management to balance memory use and speed of response in Girder when large-image is used as a tile server.

+

Most tile sources can be used with Girder Large Image. You can specify an extras_require of girder to install the following packages:

+
+
    +
  • girder-large-image: Large Image as a Girder 3.x plugin. +You can install large-image[tasks] to install a Girder Worker task that can convert otherwise unreadable images to pyramidal tiff files.

  • +
  • girder-large-image-annotation: Adds models to the Girder database for supporting annotating large images. These annotations can be rendered on images. Annotations can include polygons, points, image overlays, and other types. Each annotation can have a label and metadata.

  • +
  • large-image-tasks: A utility for running the converter via Girder Worker. +You can specify an extras_require of girder to include modules needed to work with the Girder remote worker or worker to include modules needed on the remote side of the Girder remote worker. If neither is specified, some conversion tasks can be run using Girder local jobs.

  • +
+
+ +
+
+
+

Indices and tables🔗

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/multi_source_specification.html b/multi_source_specification.html new file mode 100644 index 000000000..170322eb2 --- /dev/null +++ b/multi_source_specification.html @@ -0,0 +1,575 @@ + + + + + + + Multi Source Schema — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Multi Source Schema🔗

+

A multi-source tile source is used to composite multiple other sources into a +single conceptual tile source. It is specified by a yaml or json file that +conforms to the appropriate schema.

+
+

Examples🔗

+

All of the examples presented here are in yaml; json works just as well.

+
+

Multi Z-position🔗

+

For example, if you have a set of individual files that you wish to treat as +multiple z slices in a single file, you can do something like:

+
---
+sources:
+  - path: ./test_orient1.tif
+    z: 0
+  - path: ./test_orient2.tif
+    z: 1
+  - path: ./test_orient3.tif
+    z: 2
+  - path: ./test_orient4.tif
+    z: 3
+  - path: ./test_orient5.tif
+    z: 4
+  - path: ./test_orient6.tif
+    z: 5
+  - path: ./test_orient7.tif
+    z: 6
+  - path: ./test_orient8.tif
+    z: 7
+
+
+

Here, each of the files is explicitly listed with a specific z value. +Since these files are ordered, this could equivalently be done in a simpler +manner using a pathPattern, which is a regular expression that can match +multiple files.

+
---
+sources:
+  - path: .
+    pathPattern: 'test_orient[1-8]\.tif'
+    zStep: 1
+
+
+

Since the z value will default to 0, this works. The files are sorted in +C-sort order (lexically using the ASCII or UTF code points). This sorting will +break down if you have files with variable length numbers (e.g., file10.tif +will appear before file9.tiff. You can instead assign values from the +file name using named expressions:

+
---
+sources:
+  - path: .
+    pathPattern: 'test_orient(?P<z1>[1-8])\.tif'
+
+
+

Note that the name in the expression (z1 in this example) is the name of +the value in the schema. If a 1 is added, then it is assumed to be 1-based +indexed. Without the 1, it is assumed to be zero-indexed.

+
+
+

Composite To A Single Frame🔗

+

Multiple sources can be made to appear as a single frame. For instance:

+
---
+width: 360
+height: 360
+sources:
+  - path: ./test_orient1.tif
+    z: 0
+    position:
+      x: 0
+      y: 0
+  - path: ./test_orient2.tif
+    z: 0
+    position:
+      x: 180
+      y: 0
+  - path: ./test_orient3.tif
+    z: 0
+    position:
+      x: 0
+      y: 180
+  - path: ./test_orient4.tif
+    z: 0
+    position:
+      x: 180
+      y: 180
+
+
+

Here, the total width and height of the final image is specified, along with +the upper-left position of each image in the frame.

+
+
+

Composite With Scaling🔗

+

Transforms can be applied to scale the individual sources:

+
---
+width: 720
+height: 720
+sources:
+  - path: ./test_orient1.tif
+    position:
+      scale: 2
+  - path: ./test_orient2.tif
+    position:
+      scale: 2
+      x: 360
+  - path: ./test_orient3.tif
+    position:
+      scale: 2
+      y: 360
+  - path: ./test_orient4.tif
+    position:
+      scale: 2
+      x: 180
+      y: 180
+
+
+

Note that the zero values from the previous example have been omitted as they +are unnecessary.

+
+
+
+

Full Schema🔗

+

The full schema (jsonschema Draft6 standard) can be obtained by referencing the +Python at large_image_source_multi.MultiSourceSchema.

+

This returns the following:

+
{
+  "$schema": "http://json-schema.org/schema#",
+  "type": "object",
+  "additionalProperties": false,
+  "properties": {
+    "name": {
+      "type": "string"
+    },
+    "description": {
+      "type": "string"
+    },
+    "width": {
+      "type": "integer",
+      "exclusiveMinimum": 0
+    },
+    "height": {
+      "type": "integer",
+      "exclusiveMinimum": 0
+    },
+    "tileWidth": {
+      "type": "integer",
+      "exclusiveMinimum": 0
+    },
+    "tileHeight": {
+      "type": "integer",
+      "exclusiveMinimum": 0
+    },
+    "channels": {
+      "description": "A list of channel names",
+      "type": "array",
+      "items": {
+        "type": "string"
+      },
+      "minItems": 1
+    },
+    "scale": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "mm_x": {
+          "type": "number",
+          "exclusiveMinimum": 0
+        },
+        "mm_y": {
+          "type": "number",
+          "exclusiveMinimum": 0
+        },
+        "magnification": {
+          "type": "integer",
+          "exclusiveMinimum": 0
+        }
+      }
+    },
+    "backgroundColor": {
+      "description": "A list of background color values (fill color) in the same scale and band order as the first tile source (e.g., white might be [255, 255, 255] for a three channel image).",
+      "type": "array",
+      "items": {
+        "type": "number"
+      }
+    },
+    "basePath": {
+      "decription": "A relative path that is used as a base for all paths in sources.  Defaults to the directory of the main file.",
+      "type": "string"
+    },
+    "uniformSources": {
+      "description": "If true and the first two sources are similar in frame layout and size, assume all sources are so similar",
+      "type": "boolean"
+    },
+    "dtype": {
+      "description": "If present, a numpy dtype name to use for the data.",
+      "type": "string"
+    },
+    "singleBand": {
+      "description": "If true, output only the first band of compositied results",
+      "type": "boolean"
+    },
+    "axes": {
+      "description": "A list of additional axes that will be parsed.  The default axes are z, t, xy, and c.  It is recommended that additional axes use terse names and avoid x, y, and s.",
+      "type": "array",
+      "items": {
+        "type": "string"
+      }
+    },
+    "sources": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "additionalProperties": true,
+        "properties": {
+          "name": {
+            "type": "string"
+          },
+          "description": {
+            "type": "string"
+          },
+          "path": {
+            "decription": "The relative path, including file name if pathPattern is not specified.  The relative path excluding file name if pathPattern is specified.  Or, girder://id for Girder sources.  If a specific tile source is specified that does not need an actual path, the special value of `__none__` can be used to bypass checking for an actual file.",
+            "type": "string"
+          },
+          "pathPattern": {
+            "description": "If specified, file names in the path are matched to this regular expression, sorted in C-sort order.  This can populate other properties via named expressions, e.g., base_(?<xy>\\d+).png.  Add 1 to the name for 1-based numerical values.",
+            "type": "string"
+          },
+          "sourceName": {
+            "description": "Require a specific source by name.  This is one of the large_image source names (e.g., this one is \"multi\".",
+            "type": "string"
+          },
+          "frame": {
+            "description": "Base value for all frames; only use this if the data does not conceptually have z, t, xy, or c arrangement.",
+            "type": "integer",
+            "minimum": 0
+          },
+          "z": {
+            "description": "Base value for all frames",
+            "type": "integer",
+            "minimum": 0
+          },
+          "t": {
+            "description": "Base value for all frames",
+            "type": "integer",
+            "minimum": 0
+          },
+          "xy": {
+            "description": "Base value for all frames",
+            "type": "integer",
+            "minimum": 0
+          },
+          "c": {
+            "description": "Base value for all frames",
+            "type": "integer",
+            "minimum": 0
+          },
+          "zSet": {
+            "description": "Override value for frame",
+            "type": "integer",
+            "minimum": 0
+          },
+          "tSet": {
+            "description": "Override value for frame",
+            "type": "integer",
+            "minimum": 0
+          },
+          "xySet": {
+            "description": "Override value for frame",
+            "type": "integer",
+            "minimum": 0
+          },
+          "cSet": {
+            "description": "Override value for frame",
+            "type": "integer",
+            "minimum": 0
+          },
+          "zValues": {
+            "description": "The numerical z position of the different z indices of the source.  If only one value is specified, other indices are shifted based on the source.  If fewer values are given than z indices, the last two value given imply a stride for the remainder.",
+            "type": "array",
+            "items": {
+              "type": "number"
+            },
+            "minItems": 1
+          },
+          "tValues": {
+            "description": "The numerical t position of the different t indices of the source.  If only one value is specified, other indices are shifted based on the source.  If fewer values are given than t indices, the last two value given imply a stride for the remainder.",
+            "type": "array",
+            "items": {
+              "type": "number"
+            },
+            "minItems": 1
+          },
+          "xyValues": {
+            "description": "The numerical xy position of the different xy indices of the source.  If only one value is specified, other indices are shifted based on the source.  If fewer values are given than xy indices, the last two value given imply a stride for the remainder.",
+            "type": "array",
+            "items": {
+              "type": "number"
+            },
+            "minItems": 1
+          },
+          "cValues": {
+            "description": "The numerical c position of the different c indices of the source.  If only one value is specified, other indices are shifted based on the source.  If fewer values are given than c indices, the last two value given imply a stride for the remainder.",
+            "type": "array",
+            "items": {
+              "type": "number"
+            },
+            "minItems": 1
+          },
+          "frameValues": {
+            "description": "The numerical frame position of the different frame indices of the source.  If only one value is specified, other indices are shifted based on the source.  If fewer values are given than frame indices, the last two value given imply a stride for the remainder.",
+            "type": "array",
+            "items": {
+              "type": "number"
+            },
+            "minItems": 1
+          },
+          "channel": {
+            "description": "A channel name to correspond with the main image.  Ignored if c, cValues, or channels is specified.",
+            "type": "string"
+          },
+          "channels": {
+            "description": "A list of channel names used to correspond channels in this source with the main image.  Ignored if c or cValues is specified.",
+            "type": "array",
+            "items": {
+              "type": "string"
+            },
+            "minItems": 1
+          },
+          "zStep": {
+            "description": "Step value for multiple files included via pathPattern.  Applies to z or zValues",
+            "type": "integer",
+            "exclusiveMinimum": 0
+          },
+          "tStep": {
+            "description": "Step value for multiple files included via pathPattern.  Applies to t or tValues",
+            "type": "integer",
+            "exclusiveMinimum": 0
+          },
+          "xyStep": {
+            "description": "Step value for multiple files included via pathPattern.  Applies to x or xyValues",
+            "type": "integer",
+            "exclusiveMinimum": 0
+          },
+          "xStep": {
+            "description": "Step value for multiple files included via pathPattern.  Applies to c or cValues",
+            "type": "integer",
+            "exclusiveMinimum": 0
+          },
+          "framesAsAxes": {
+            "description": "An object with keys as axes and values as strides to interpret the source frames.  This overrides the internal metadata for frames.",
+            "type": "object",
+            "patternProperties": {
+              "^(c|t|z|xy)$": {
+                "type": "integer",
+                "exclusiveMinimum": 0
+              }
+            },
+            "additionalProperties": false
+          },
+          "position": {
+            "type": "object",
+            "additionalProperties": false,
+            "description": "The image can be translated with x, y offset, apply an affine transform, and scaled.  If only part of the source is desired, a crop can be applied before the transformation.",
+            "properties": {
+              "x": {
+                "type": "number"
+              },
+              "y": {
+                "type": "number"
+              },
+              "crop": {
+                "description": "Crop the source before applying a position transform",
+                "type": "object",
+                "additionalProperties": false,
+                "properties": {
+                  "left": {
+                    "type": "integer"
+                  },
+                  "top": {
+                    "type": "integer"
+                  },
+                  "right": {
+                    "type": "integer"
+                  },
+                  "bottom": {
+                    "type": "integer"
+                  }
+                }
+              },
+              "scale": {
+                "description": "Values less than 1 will downsample the source.  Values greater than 1 will upsample it.",
+                "type": "number",
+                "exclusiveMinimum": 0
+              },
+              "s11": {
+                "type": "number"
+              },
+              "s12": {
+                "type": "number"
+              },
+              "s21": {
+                "type": "number"
+              },
+              "s22": {
+                "type": "number"
+              }
+            }
+          },
+          "frames": {
+            "description": "List of frames to use from source",
+            "type": "array",
+            "items": {
+              "type": "integer"
+            }
+          },
+          "sampleScale": {
+            "description": "Each pixel sample values is divided by this scale after any sampleOffset has been applied",
+            "type": "number"
+          },
+          "sampleOffset": {
+            "description": "This is added to each pixel sample value before any sampleScale is applied",
+            "type": "number"
+          },
+          "style": {
+            "type": "object"
+          },
+          "params": {
+            "description": "Additional parameters to pass to the base tile source",
+            "type": "object"
+          }
+        },
+        "required": [
+          "path"
+        ]
+      }
+    }
+  },
+  "required": [
+    "sources"
+  ]
+}
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/notebooks.html b/notebooks.html new file mode 100644 index 000000000..636271056 --- /dev/null +++ b/notebooks.html @@ -0,0 +1,145 @@ + + + + + + + Jupyter Notebook Examples — large_image documentation + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Jupyter Notebook Examples🔗

+

These example notebooks demonstrate some basic large-image functionality used in Jupyter Notebooks.

+

You can view static versions of these example notebooks here (these do not include dynamic image viewers):

+ +

To see the dynamic image viewers, you can run these example notebooks in any Jupyter environment. +To download any of these example notebooks, visit the notebooks folder in our Github repository. +Or, you can run any of these notebooks in Google Colab with the following links:

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/notebooks/large_image_examples.html b/notebooks/large_image_examples.html new file mode 100644 index 000000000..a869f5982 --- /dev/null +++ b/notebooks/large_image_examples.html @@ -0,0 +1,629 @@ + + + + + + + Using Large Image in Jupyter — large_image documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Using Large Image in Jupyter🔗

+

The large_image library has some convenience features for use in Jupyter Notebooks and Jupyter Lab. Different features are available depending on whether your data files are local or on a Girder server.

+
+

Installation🔗

+

The large_image library has a variety of tile sources to support a wide range of file formats. Many of these depend on binary libraries. For linux systems, you can install these from python wheels via the --find-links option. For other operating systems, you will need to install different libraries depending on what tile sources you wish to use.

+
+
[1]:
+
+
+
# This will install large_image, including all sources and many other options
+!pip install large_image[all] --find-links https://girder.github.io/large_image_wheels
+# For a smaller set of tile sources, you could also do:
+# !pip install large_image[pil,rasterio,tifffile]
+
+# For maximum capabilities in Jupyter, also install ipyleaflet so you can
+# view zoomable images in the notebook
+!pip install ipyleaflet
+
+# If you are accessing files on a Girder server, it is useful to install girder_client
+!pip install girder_client
+
+
+
+
+
+
+
+
+Looking in links: https://girder.github.io/large_image_wheels
+
+
+
+
+

Using Local Files🔗

+

When using large_image with local files, when you open a file, large_image returns a tile source. See girder.github.io/large_image for documentation on what you can do with this.

+

First, we download a few files so we can use them locally.

+
+
[2]:
+
+
+
# Get a few files so we can use them locally
+!curl -L -C - -o TC_NG_SFBay_US_Geo_COG.tif https://data.kitware.com/api/v1/file/hashsum/sha512/5e56cdb8fb1a02615698a153862c10d5292b1ad42836a6e8bce5627e93a387dc0d3c9b6cfbd539796500bc2d3e23eafd07550f8c214e9348880bbbc6b3b0ea0c/download
+!curl -L -C - -o TCGA-AA-A02O-11A-01-BS1.svs https://data.kitware.com/api/v1/file/hashsum/sha512/1b75a4ec911017aef5c885760a3c6575dacf5f8efb59fb0e011108dce85b1f4e97b8d358f3363c1f5ea6f1c3698f037554aec1620bbdd4cac54e3d5c9c1da1fd/download
+
+
+
+
+
+
+
+
+  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100 32.8M  100 32.8M    0     0   103M      0 --:--:-- --:--:-- --:--:--  103M
+  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100 59.0M  100 59.0M    0     0  96.9M      0 --:--:-- --:--:-- --:--:-- 96.8M
+
+
+
+
+

Basic Use🔗

+

The large_image library has a variety of tile sources that support a wide range of formats. In general, you don’t need to know the format of a file, you can just open it.

+

Every file has a common interface regardless of its format. The metadata gives a common summary of the data.

+
+
[3]:
+
+
+
import large_image
+
+ts = large_image.open('TCGA-AA-A02O-11A-01-BS1.svs')
+# The thumbnail method returns a tuple with an image or numpy array and a mime type
+ts.getThumbnail()[0]
+
+
+
+
+
[3]:
+
+
+
+../_images/notebooks_large_image_examples_6_0.jpg +
+
+
+
[4]:
+
+
+
# Every image's dimensions are in `sizeX` and `sizeY`.  If known, a variety of other information
+# is provided.
+ts.metadata
+
+
+
+
+
[4]:
+
+
+
+
+{'levels': 9,
+ 'sizeX': 55988,
+ 'sizeY': 16256,
+ 'tileWidth': 256,
+ 'tileHeight': 256,
+ 'magnification': 20.0,
+ 'mm_x': 0.0004991,
+ 'mm_y': 0.0004991,
+ 'dtype': 'uint8',
+ 'bandCount': 4}
+
+
+
+
If you have ipyleaflet installed and are using JupyterLab, you can ask the system to proxy requests to an internal tile server that allows you to view the image in a zoomable viewer. There are more options depending on your Jupyter configuration and whether it is running locally or remotely.
+
Some environments need different proxy options, like Google CoLab.
+
+

If ipyleaflet isn’t installed, inspecting a tile source will just show the thumbnail.

+
+
[5]:
+
+
+
# Ask JupyterLab to locally proxy an internal tile server
+import importlib.util
+
+if importlib.util.find_spec('google') and importlib.util.find_spec('google.colab'):
+    # colab intercepts localhost
+    large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = 'https://localhost'
+else:
+    large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = True
+
+# Look at our tile source
+ts
+
+
+
+
+
+
+
+
+
+

If you see a black border on the right and bottom, this is because the ipyleaflet viewer shows areas outside the bounds of the image. We could ask for the image to be served using PNG images so that those areas are transparent

+
+
[6]:
+
+
+
ts = large_image.open('TCGA-AA-A02O-11A-01-BS1.svs', encoding='PNG')
+ts
+
+
+
+
+
+
+
+
+
+

The IPyLeaflet map uses a bottom-up y, x coordinate system, not the top-down x, y coordinate system most image system use. The rationale is that this is appropriate for geospatial maps with latitude and longitude, but it doesn’t carry over to pixel coordinates very well. There are some convenience functions to convert coordinates.

+
+
[7]:
+
+
+
import ipyleaflet
+
+# Get a reference to the IPyLeaflet Map
+map = ts.iplmap
+# to_map converts pixel coordinates to IPyLeaflet map coordinates.
+# draw a rectangle that is wider than tall.
+rectangle = ipyleaflet.Rectangle(bounds=(ts.to_map((0, 0)), ts.to_map((10000, 5000))))
+map.add_layer(rectangle)
+# draw another rectangle that is the size of the whole image.
+rectangle = ipyleaflet.Rectangle(bounds=(ts.to_map((0, 0)), ts.to_map((ts.sizeX, ts.sizeY))))
+map.add_layer(rectangle)
+# show the map
+map
+
+
+
+
+
[7]:
+
+
+
+
+
+
+
+

Geospatial Sources🔗

+

For geospatial sources, the default viewer shows the image in context on a world map if an appropriate projection is used.

+
+
[8]:
+
+
+
geots = large_image.open('TC_NG_SFBay_US_Geo_COG.tif', projection='EPSG:3857', encoding='PNG')
+geots
+
+
+
+
+
+
+
+
+
+

Geospatial sources have additional metadata and thumbnails.

+
+
[9]:
+
+
+
geots.metadata
+
+
+
+
+
[9]:
+
+
+
+
+{'levels': 15,
+ 'sizeX': 4194304,
+ 'sizeY': 4194304,
+ 'tileWidth': 256,
+ 'tileHeight': 256,
+ 'magnification': None,
+ 'mm_x': 1381.876143450579,
+ 'mm_y': 1381.876143450579,
+ 'dtype': 'uint8',
+ 'bandCount': 3,
+ 'geospatial': True,
+ 'sourceLevels': 6,
+ 'sourceSizeX': 4323,
+ 'sourceSizeY': 4323,
+ 'bounds': {'ll': {'x': -13660993.43811085, 'y': 4502326.297712617},
+  'ul': {'x': -13660993.43811085, 'y': 4586806.951318035},
+  'lr': {'x': -13594198.136883384, 'y': 4502326.297712617},
+  'ur': {'x': -13594198.136883384, 'y': 4586806.951318035},
+  'srs': 'epsg:3857',
+  'xmin': -13660993.43811085,
+  'xmax': -13594198.136883384,
+  'ymin': 4502326.297712617,
+  'ymax': 4586806.951318035},
+ 'projection': 'epsg:3857',
+ 'sourceBounds': {'ll': {'x': -122.71879201711467, 'y': 37.45219874192699},
+  'ul': {'x': -122.71879201711467, 'y': 38.052231141926995},
+  'lr': {'x': -122.11875961711466, 'y': 37.45219874192699},
+  'ur': {'x': -122.11875961711466, 'y': 38.052231141926995},
+  'srs': '+proj=longlat +datum=WGS84 +no_defs',
+  'xmin': -122.71879201711467,
+  'xmax': -122.11875961711466,
+  'ymin': 37.45219874192699,
+  'ymax': 38.052231141926995},
+ 'bands': {1: {'min': 5.0,
+   'max': 255.0,
+   'mean': 56.164648651261,
+   'stdev': 45.505628098154,
+   'interpretation': 'red'},
+  2: {'min': 2.0,
+   'max': 255.0,
+   'mean': 61.590676043792,
+   'stdev': 35.532493975171,
+   'interpretation': 'green'},
+  3: {'min': 1.0,
+   'max': 255.0,
+   'mean': 47.00898008224,
+   'stdev': 29.470217162239,
+   'interpretation': 'blue'}}}
+
+
+
+
[10]:
+
+
+
geots.getThumbnail()[0]
+
+
+
+
+
[10]:
+
+
+
+../_images/notebooks_large_image_examples_18_0.jpg +
+
+
+
+

Girder Server Sources🔗

+

You can use files on a Girder server by just download them and using them locally. However, you can use girder client to access files more conveniently. If the Girder server doesn’t have the large_image plugin installed on it, this can still be useful – functionally, this pulls the file and provides a local tile server, so some of this requires the same proxy setup as a local file.

+

large_image.tilesource.jupyter.Map is a convenience class that can use a variety of remote sources.

+

(1) We can get a source from girder via item or file id

+
+
[11]:
+
+
+
import girder_client
+
+gc1 = girder_client.GirderClient(apiUrl='https://data.kitware.com/api/v1')
+# If you need to authenticate, an easy way is to ask directly
+# gc.authenticate(interactive=True)
+# but you could also use an API token or a variety of other methods.
+
+# We can ask for the image by item or file id
+map1 = large_image.tilesource.jupyter.Map(gc=gc1, id='57b345d28d777f126827dc28')
+map1
+
+
+
+
+
+
+
+
+
+

(2) We could use a resource path instead of an id

+
+
[12]:
+
+
+
map2 = large_image.tilesource.jupyter.Map(gc=gc1, resource='/collection/HistomicsTK/CI and tox Test Data/large_image test files/Huron.Image2_JPEG2K.tif')
+map2
+
+
+
+
+
+
+
+
+
+
+
[13]:
+
+
+
# You can get an id of an item using pure girder client calls, too.  For instance, internally, the
+# id is fetched from the resource path and then used.
+resourceFromMap2 = '/collection/HistomicsTK/CI and tox Test Data/large_image test files/Huron.Image2_JPEG2K.tif'
+idOfResource = gc1.get('resource/lookup', parameters={'path': resourceFromMap2})['_id']
+idOfResource
+
+
+
+
+
[13]:
+
+
+
+
+'5818e9418d777f10f26ee443'
+
+
+

(3) We can use a girder server that has the large_image plugin enabled. This lets us do more than just look at the image.

+
+
[14]:
+
+
+
gc2 = girder_client.GirderClient(apiUrl='https://demo.kitware.com/histomicstk/api/v1')
+
+resourcePath = '/collection/Crowd Source Paper/All slides/TCGA-A1-A0SP-01Z-00-DX1.20D689C6-EFA5-4694-BE76-24475A89ACC0.svs'
+map3 = large_image.tilesource.jupyter.Map(gc=gc2, resource=resourcePath)
+map3
+
+
+
+
+
+
+
+
+
+
+
[15]:
+
+
+
# We can check the metadata
+map3.metadata
+
+
+
+
+
[15]:
+
+
+
+
+{'dtype': 'uint8',
+ 'levels': 10,
+ 'magnification': 40.0,
+ 'mm_x': 0.0002521,
+ 'mm_y': 0.0002521,
+ 'sizeX': 109434,
+ 'sizeY': 90504,
+ 'tileHeight': 256,
+ 'tileWidth': 256}
+
+
+

We can get data as a numpy array.

+
+
[16]:
+
+
+
import pickle
+
+pickle.loads(gc2.get(f'item/{map3.id}/tiles/region', parameters={'encoding': 'pickle', 'width': 100, 'height': 100},  jsonResp=False).content)
+
+
+
+
+
[16]:
+
+
+
+
+array([[[240, 242, 241, 255],
+        [240, 242, 241, 255],
+        [241, 242, 242, 255],
+        ...,
+        [238, 240, 239, 253],
+        [239, 241, 240, 255],
+        [239, 241, 240, 255]],
+
+       [[240, 241, 240, 255],
+        [239, 241, 240, 255],
+        [240, 241, 240, 255],
+        ...,
+        [237, 238, 238, 253],
+        [237, 239, 238, 255],
+        [237, 239, 238, 255]],
+
+       [[239, 241, 240, 255],
+        [239, 241, 240, 255],
+        [239, 241, 240, 255],
+        ...,
+        [236, 238, 237, 253],
+        [237, 239, 238, 255],
+        [237, 239, 238, 255]],
+
+       ...,
+
+       [[240, 241, 241, 255],
+        [240, 241, 241, 255],
+        [240, 241, 241, 255],
+        ...,
+        [239, 240, 239, 253],
+        [240, 241, 240, 255],
+        [239, 241, 240, 255]],
+
+       [[241, 243, 242, 255],
+        [241, 242, 242, 255],
+        [241, 242, 242, 255],
+        ...,
+        [238, 241, 240, 253],
+        [239, 242, 241, 255],
+        [239, 241, 241, 255]],
+
+       [[237, 239, 240, 253],
+        [237, 240, 240, 253],
+        [236, 239, 239, 253],
+        ...,
+        [234, 237, 238, 251],
+        [234, 237, 237, 253],
+        [235, 238, 238, 253]]], dtype=uint8)
+
+
+

(4) From a metadata dictionary and a url. Any slippy-map style tile server could be used.

+
+
[17]:
+
+
+
# There can be additional items in the metadata, but this is minimum required.
+remoteMetadata = {
+  'levels': 10,
+  'sizeX': 95758,
+  'sizeY': 76873,
+  'tileHeight': 256,
+  'tileWidth': 256,
+}
+remoteUrl = 'https://demo.kitware.com/histomicstk/api/v1/item/5bbdeec6e629140048d01bb9/tiles/zxy/{z}/{x}/{y}?encoding=PNG'
+
+map4 = large_image.tilesource.jupyter.Map(metadata=remoteMetadata, url=remoteUrl)
+map4
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/notebooks/large_image_examples.ipynb b/notebooks/large_image_examples.ipynb new file mode 100644 index 000000000..34a2577e3 --- /dev/null +++ b/notebooks/large_image_examples.ipynb @@ -0,0 +1,866 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "73529f76-83b2-4d2c-b8f3-01bd9c1696af", + "metadata": {}, + "source": [ + "Using Large Image in Jupyter\n", + "============================\n", + "\n", + "The large_image library has some convenience features for use in Jupyter Notebooks and Jupyter Lab. Different features are available depending on whether your data files are local or on a Girder server." + ] + }, + { + "cell_type": "markdown", + "id": "ffb9e79e-2d89-4e41-92cb-ee736833d309", + "metadata": {}, + "source": [ + "Installation\n", + "------------\n", + "\n", + "The large_image library has a variety of tile sources to support a wide range of file formats. Many of these depend\n", + "on binary libraries. For linux systems, you can install these from python wheels via the `--find-links` option. For\n", + "other operating systems, you will need to install different libraries depending on what tile sources you wish to use." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "fa38be1a-341a-4725-98f0-b61318fc696a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in links: https://girder.github.io/large_image_wheels\n" + ] + } + ], + "source": [ + "# This will install large_image, including all sources and many other options\n", + "!pip install large_image[all] --find-links https://girder.github.io/large_image_wheels\n", + "# For a smaller set of tile sources, you could also do:\n", + "# !pip install large_image[pil,rasterio,tifffile]\n", + "\n", + "# For maximum capabilities in Jupyter, also install ipyleaflet so you can\n", + "# view zoomable images in the notebook\n", + "!pip install ipyleaflet\n", + "\n", + "# If you are accessing files on a Girder server, it is useful to install girder_client\n", + "!pip install girder_client" + ] + }, + { + "cell_type": "markdown", + "id": "c9a14ff3-4c28-49af-ad71-565f420770c9", + "metadata": {}, + "source": [ + "Using Local Files\n", + "-----------------\n", + "\n", + "When using large_image with local files, when you open a file, large_image returns a tile source. See [girder.github.io/large_image](https://girder.github.io/large_image) for documentation on what you can do with this.\n", + "\n", + "First, we download a few files so we can use them locally." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "73409e8c-08b3-4891-a7fc-c5c42e453ffb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " % Total % Received % Xferd Average Speed Time Time Time Current\n", + " Dload Upload Total Spent Left Speed\n", + "100 32.8M 100 32.8M 0 0 103M 0 --:--:-- --:--:-- --:--:-- 103M\n", + " % Total % Received % Xferd Average Speed Time Time Time Current\n", + " Dload Upload Total Spent Left Speed\n", + "100 59.0M 100 59.0M 0 0 96.9M 0 --:--:-- --:--:-- --:--:-- 96.8M\n" + ] + } + ], + "source": [ + "# Get a few files so we can use them locally\n", + "!curl -L -C - -o TC_NG_SFBay_US_Geo_COG.tif https://data.kitware.com/api/v1/file/hashsum/sha512/5e56cdb8fb1a02615698a153862c10d5292b1ad42836a6e8bce5627e93a387dc0d3c9b6cfbd539796500bc2d3e23eafd07550f8c214e9348880bbbc6b3b0ea0c/download\n", + "!curl -L -C - -o TCGA-AA-A02O-11A-01-BS1.svs https://data.kitware.com/api/v1/file/hashsum/sha512/1b75a4ec911017aef5c885760a3c6575dacf5f8efb59fb0e011108dce85b1f4e97b8d358f3363c1f5ea6f1c3698f037554aec1620bbdd4cac54e3d5c9c1da1fd/download" + ] + }, + { + "cell_type": "markdown", + "id": "09722713-e1e8-4ae2-939d-e9aa996e4c42", + "metadata": {}, + "source": [ + "Basic Use\n", + "---------\n", + "The large_image library has a variety of tile sources that support a wide range of formats.\n", + "In general, you don't need to know the format of a file, you can just open it.\n", + "\n", + "Every file has a common interface regardless of its format. The metadata gives a common summary of the data." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "525e98e6-103b-4b95-becc-c23931f17873", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "ImageBytes<5146> (image/jpeg)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import large_image\n", + "\n", + "ts = large_image.open('TCGA-AA-A02O-11A-01-BS1.svs')\n", + "# The thumbnail method returns a tuple with an image or numpy array and a mime type\n", + "ts.getThumbnail()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "6e3ee887-a21f-426b-b221-9e3504d75870", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "bandCount": 4, + "dtype": "uint8", + "levels": 9, + "magnification": 20, + "mm_x": 0.0004991, + "mm_y": 0.0004991, + "sizeX": 55988, + "sizeY": 16256, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'levels': 9,\n", + " 'sizeX': 55988,\n", + " 'sizeY': 16256,\n", + " 'tileWidth': 256,\n", + " 'tileHeight': 256,\n", + " 'magnification': 20.0,\n", + " 'mm_x': 0.0004991,\n", + " 'mm_y': 0.0004991,\n", + " 'dtype': 'uint8',\n", + " 'bandCount': 4}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Every image's dimensions are in `sizeX` and `sizeY`. If known, a variety of other information\n", + "# is provided.\n", + "ts.metadata" + ] + }, + { + "cell_type": "markdown", + "id": "27c92320-3c21-40a0-89e0-266ee6850c4c", + "metadata": {}, + "source": [ + "If you have ipyleaflet installed and are using JupyterLab, you can ask the system to proxy requests\n", + "to an internal tile server that allows you to view the image in a zoomable viewer. There are more options\n", + "depending on your Jupyter configuration and whether it is running locally or remotely. \n", + "Some environments need different proxy options, like Google CoLab.\n", + "\n", + "If ipyleaflet isn't installed, inspecting a tile source will just show the thumbnail." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c0b16fe7-5237-4fdb-9bd4-9b017c7abc8c", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "48f48c57d0454472bf135cf1fac22cea", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[8128.0, 27994.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Ask JupyterLab to locally proxy an internal tile server\n", + "import importlib.util\n", + "\n", + "if importlib.util.find_spec('google') and importlib.util.find_spec('google.colab'):\n", + " # colab intercepts localhost\n", + " large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = 'https://localhost'\n", + "else:\n", + " large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = True\n", + "\n", + "# Look at our tile source\n", + "ts" + ] + }, + { + "cell_type": "markdown", + "id": "565cd319-7a07-4fe4-9160-b4ec84671821", + "metadata": {}, + "source": [ + "If you see a black border on the right and bottom, this is because the ipyleaflet viewer shows areas\n", + "outside the bounds of the image. We could ask for the image to be served using PNG images so that those\n", + "areas are transparent" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "25a0538f-8bfb-4079-843e-ba7732d5103c", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3b6a54aae908468880216fee266860f4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[8128.0, 27994.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ts = large_image.open('TCGA-AA-A02O-11A-01-BS1.svs', encoding='PNG')\n", + "ts" + ] + }, + { + "cell_type": "markdown", + "id": "79d07b59-05da-41f7-89ac-584e057825bf", + "metadata": {}, + "source": [ + "The IPyLeaflet map uses a bottom-up y, x coordinate system, not the top-down x, y coordinate system \n", + "most image system use. The rationale is that this is appropriate for geospatial maps with\n", + "latitude and longitude, but it doesn't carry over to pixel coordinates very well. There are some\n", + "convenience functions to convert coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0a1f6720-e4fc-47ea-8d35-158a25516b9f", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3b6a54aae908468880216fee266860f4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(bottom=232.0, center=[8128.0, 27994.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_i…" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import ipyleaflet\n", + "\n", + "# Get a reference to the IPyLeaflet Map\n", + "map = ts.iplmap\n", + "# to_map converts pixel coordinates to IPyLeaflet map coordinates.\n", + "# draw a rectangle that is wider than tall.\n", + "rectangle = ipyleaflet.Rectangle(bounds=(ts.to_map((0, 0)), ts.to_map((10000, 5000))))\n", + "map.add_layer(rectangle)\n", + "# draw another rectangle that is the size of the whole image.\n", + "rectangle = ipyleaflet.Rectangle(bounds=(ts.to_map((0, 0)), ts.to_map((ts.sizeX, ts.sizeY))))\n", + "map.add_layer(rectangle)\n", + "# show the map\n", + "map" + ] + }, + { + "cell_type": "markdown", + "id": "510883e6-2182-4959-852f-86357816ad57", + "metadata": {}, + "source": [ + "Geospatial Sources\n", + "------------------\n", + "\n", + "For geospatial sources, the default viewer shows the image in context on a world map if an appropriate projection is used." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "81556073-6db9-41f8-aa9f-1757845aedf2", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5150c395482d40fbb80df6cea8fcc4ea", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[37.752214941926994, -122.41877581711466], controls=(ZoomControl(options=['position', 'zoom_in_text…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "geots = large_image.open('TC_NG_SFBay_US_Geo_COG.tif', projection='EPSG:3857', encoding='PNG')\n", + "geots" + ] + }, + { + "cell_type": "markdown", + "id": "c58bf0a5-dfc7-4e5e-bfc0-e243acfb6313", + "metadata": {}, + "source": [ + "Geospatial sources have additional metadata and thumbnails." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "5378671a-1374-4f42-822c-94f89cbaa267", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "bandCount": 3, + "bands": { + "1": { + "interpretation": "red", + "max": 255, + "mean": 56.164648651261, + "min": 5, + "stdev": 45.505628098154 + }, + "2": { + "interpretation": "green", + "max": 255, + "mean": 61.590676043792, + "min": 2, + "stdev": 35.532493975171 + }, + "3": { + "interpretation": "blue", + "max": 255, + "mean": 47.00898008224, + "min": 1, + "stdev": 29.470217162239 + } + }, + "bounds": { + "ll": { + "x": -13660993.43811085, + "y": 4502326.297712617 + }, + "lr": { + "x": -13594198.136883384, + "y": 4502326.297712617 + }, + "srs": "epsg:3857", + "ul": { + "x": -13660993.43811085, + "y": 4586806.951318035 + }, + "ur": { + "x": -13594198.136883384, + "y": 4586806.951318035 + }, + "xmax": -13594198.136883384, + "xmin": -13660993.43811085, + "ymax": 4586806.951318035, + "ymin": 4502326.297712617 + }, + "dtype": "uint8", + "geospatial": true, + "levels": 15, + "magnification": null, + "mm_x": 1381.876143450579, + "mm_y": 1381.876143450579, + "projection": "epsg:3857", + "sizeX": 4194304, + "sizeY": 4194304, + "sourceBounds": { + "ll": { + "x": -122.71879201711468, + "y": 37.45219874192699 + }, + "lr": { + "x": -122.11875961711466, + "y": 37.45219874192699 + }, + "srs": "+proj=longlat +datum=WGS84 +no_defs", + "ul": { + "x": -122.71879201711468, + "y": 38.052231141926995 + }, + "ur": { + "x": -122.11875961711466, + "y": 38.052231141926995 + }, + "xmax": -122.11875961711466, + "xmin": -122.71879201711468, + "ymax": 38.052231141926995, + "ymin": 37.45219874192699 + }, + "sourceLevels": 6, + "sourceSizeX": 4323, + "sourceSizeY": 4323, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'levels': 15,\n", + " 'sizeX': 4194304,\n", + " 'sizeY': 4194304,\n", + " 'tileWidth': 256,\n", + " 'tileHeight': 256,\n", + " 'magnification': None,\n", + " 'mm_x': 1381.876143450579,\n", + " 'mm_y': 1381.876143450579,\n", + " 'dtype': 'uint8',\n", + " 'bandCount': 3,\n", + " 'geospatial': True,\n", + " 'sourceLevels': 6,\n", + " 'sourceSizeX': 4323,\n", + " 'sourceSizeY': 4323,\n", + " 'bounds': {'ll': {'x': -13660993.43811085, 'y': 4502326.297712617},\n", + " 'ul': {'x': -13660993.43811085, 'y': 4586806.951318035},\n", + " 'lr': {'x': -13594198.136883384, 'y': 4502326.297712617},\n", + " 'ur': {'x': -13594198.136883384, 'y': 4586806.951318035},\n", + " 'srs': 'epsg:3857',\n", + " 'xmin': -13660993.43811085,\n", + " 'xmax': -13594198.136883384,\n", + " 'ymin': 4502326.297712617,\n", + " 'ymax': 4586806.951318035},\n", + " 'projection': 'epsg:3857',\n", + " 'sourceBounds': {'ll': {'x': -122.71879201711467, 'y': 37.45219874192699},\n", + " 'ul': {'x': -122.71879201711467, 'y': 38.052231141926995},\n", + " 'lr': {'x': -122.11875961711466, 'y': 37.45219874192699},\n", + " 'ur': {'x': -122.11875961711466, 'y': 38.052231141926995},\n", + " 'srs': '+proj=longlat +datum=WGS84 +no_defs',\n", + " 'xmin': -122.71879201711467,\n", + " 'xmax': -122.11875961711466,\n", + " 'ymin': 37.45219874192699,\n", + " 'ymax': 38.052231141926995},\n", + " 'bands': {1: {'min': 5.0,\n", + " 'max': 255.0,\n", + " 'mean': 56.164648651261,\n", + " 'stdev': 45.505628098154,\n", + " 'interpretation': 'red'},\n", + " 2: {'min': 2.0,\n", + " 'max': 255.0,\n", + " 'mean': 61.590676043792,\n", + " 'stdev': 35.532493975171,\n", + " 'interpretation': 'green'},\n", + " 3: {'min': 1.0,\n", + " 'max': 255.0,\n", + " 'mean': 47.00898008224,\n", + " 'stdev': 29.470217162239,\n", + " 'interpretation': 'blue'}}}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "geots.metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e89565cb-bbe3-4958-a691-f24aa538083d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "ImageBytes<33608> (image/jpeg)" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "geots.getThumbnail()[0]" + ] + }, + { + "cell_type": "markdown", + "id": "f236bd95-fd77-4d37-9749-004f40fb8470", + "metadata": {}, + "source": [ + "Girder Server Sources\n", + "---------------------\n", + "\n", + "You can use files on a Girder server by just download them and using them locally.\n", + "However, you can use girder client to access files more conveniently. If the Girder server\n", + "doesn't have the large_image plugin installed on it, this can still be useful -- functionally,\n", + "this pulls the file and provides a local tile server, so some of this requires the same\n", + "proxy setup as a local file.\n", + "\n", + "`large_image.tilesource.jupyter.Map` is a convenience class that can use a variety of remote sources.\n", + "\n", + "**(1)** We can get a source from girder via item or file id" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "9ebe43fb-affa-43ab-be42-064bd75bcbf7", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "04fae714b34f46be91a859c8dc0c9768", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[6917.5, 15936.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import girder_client\n", + "\n", + "gc1 = girder_client.GirderClient(apiUrl='https://data.kitware.com/api/v1')\n", + "# If you need to authenticate, an easy way is to ask directly\n", + "# gc.authenticate(interactive=True)\n", + "# but you could also use an API token or a variety of other methods.\n", + "\n", + "# We can ask for the image by item or file id\n", + "map1 = large_image.tilesource.jupyter.Map(gc=gc1, id='57b345d28d777f126827dc28')\n", + "map1" + ] + }, + { + "cell_type": "markdown", + "id": "707114c4-2cd0-4d86-a41d-21105a8761b7", + "metadata": {}, + "source": [ + "**(2)** We could use a resource path instead of an id" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "a28637e4-5c34-4b59-8618-5c9e7908b00c", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "554b0d4fa33545e7992e86976de34e02", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[5636.5, 4579.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zoom…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "map2 = large_image.tilesource.jupyter.Map(gc=gc1, resource='/collection/HistomicsTK/CI and tox Test Data/large_image test files/Huron.Image2_JPEG2K.tif')\n", + "map2" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1ea9cdae-57b1-4708-8f9e-41da933b90e2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'5818e9418d777f10f26ee443'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# You can get an id of an item using pure girder client calls, too. For instance, internally, the\n", + "# id is fetched from the resource path and then used.\n", + "resourceFromMap2 = '/collection/HistomicsTK/CI and tox Test Data/large_image test files/Huron.Image2_JPEG2K.tif'\n", + "idOfResource = gc1.get('resource/lookup', parameters={'path': resourceFromMap2})['_id']\n", + "idOfResource" + ] + }, + { + "cell_type": "markdown", + "id": "535a3990-62e1-4063-bc5f-edf5494b114f", + "metadata": {}, + "source": [ + "**(3)** We can use a girder server that has the large_image plugin enabled. This lets us do more than\n", + "just look at the image." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "b9611e09", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "aec5a5161aad4ebf9273d13ccdcc4dd5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[45252.0, 54717.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gc2 = girder_client.GirderClient(apiUrl='https://demo.kitware.com/histomicstk/api/v1')\n", + "\n", + "resourcePath = '/collection/Crowd Source Paper/All slides/TCGA-A1-A0SP-01Z-00-DX1.20D689C6-EFA5-4694-BE76-24475A89ACC0.svs'\n", + "map3 = large_image.tilesource.jupyter.Map(gc=gc2, resource=resourcePath)\n", + "map3" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "48d263ec-e350-43f4-9b2f-0c7bfb508e02", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "dtype": "uint8", + "levels": 10, + "magnification": 40, + "mm_x": 0.0002521, + "mm_y": 0.0002521, + "sizeX": 109434, + "sizeY": 90504, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'dtype': 'uint8',\n", + " 'levels': 10,\n", + " 'magnification': 40.0,\n", + " 'mm_x': 0.0002521,\n", + " 'mm_y': 0.0002521,\n", + " 'sizeX': 109434,\n", + " 'sizeY': 90504,\n", + " 'tileHeight': 256,\n", + " 'tileWidth': 256}" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# We can check the metadata\n", + "map3.metadata" + ] + }, + { + "cell_type": "markdown", + "id": "3ade5165-4628-4e5d-a601-6dd70fcb9190", + "metadata": {}, + "source": [ + "We can get data as a numpy array." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "d5ad935a-3cef-41cf-95ed-3a8b79679b93", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([[[240, 242, 241, 255],\n", + " [240, 242, 241, 255],\n", + " [241, 242, 242, 255],\n", + " ...,\n", + " [238, 240, 239, 253],\n", + " [239, 241, 240, 255],\n", + " [239, 241, 240, 255]],\n", + "\n", + " [[240, 241, 240, 255],\n", + " [239, 241, 240, 255],\n", + " [240, 241, 240, 255],\n", + " ...,\n", + " [237, 238, 238, 253],\n", + " [237, 239, 238, 255],\n", + " [237, 239, 238, 255]],\n", + "\n", + " [[239, 241, 240, 255],\n", + " [239, 241, 240, 255],\n", + " [239, 241, 240, 255],\n", + " ...,\n", + " [236, 238, 237, 253],\n", + " [237, 239, 238, 255],\n", + " [237, 239, 238, 255]],\n", + "\n", + " ...,\n", + "\n", + " [[240, 241, 241, 255],\n", + " [240, 241, 241, 255],\n", + " [240, 241, 241, 255],\n", + " ...,\n", + " [239, 240, 239, 253],\n", + " [240, 241, 240, 255],\n", + " [239, 241, 240, 255]],\n", + "\n", + " [[241, 243, 242, 255],\n", + " [241, 242, 242, 255],\n", + " [241, 242, 242, 255],\n", + " ...,\n", + " [238, 241, 240, 253],\n", + " [239, 242, 241, 255],\n", + " [239, 241, 241, 255]],\n", + "\n", + " [[237, 239, 240, 253],\n", + " [237, 240, 240, 253],\n", + " [236, 239, 239, 253],\n", + " ...,\n", + " [234, 237, 238, 251],\n", + " [234, 237, 237, 253],\n", + " [235, 238, 238, 253]]], dtype=uint8)" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pickle\n", + "\n", + "pickle.loads(gc2.get(f'item/{map3.id}/tiles/region', parameters={'encoding': 'pickle', 'width': 100, 'height': 100}, jsonResp=False).content)\n" + ] + }, + { + "cell_type": "markdown", + "id": "a5e0f551-eb64-4a1b-b28a-d2c54854fab8", + "metadata": {}, + "source": [ + "**(4)** From a metadata dictionary and a url. Any slippy-map style tile server could be used." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "ef8e1818-cafc-4e0a-bf62-a7d2b6d8f453", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "785dccbcaeb54d6d9921acf13aca24fd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Map(center=[38436.5, 47879.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', 'zo…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# There can be additional items in the metadata, but this is minimum required.\n", + "remoteMetadata = {\n", + " 'levels': 10,\n", + " 'sizeX': 95758,\n", + " 'sizeY': 76873,\n", + " 'tileHeight': 256,\n", + " 'tileWidth': 256,\n", + "}\n", + "remoteUrl = 'https://demo.kitware.com/histomicstk/api/v1/item/5bbdeec6e629140048d01bb9/tiles/zxy/{z}/{x}/{y}?encoding=PNG'\n", + "\n", + "map4 = large_image.tilesource.jupyter.Map(metadata=remoteMetadata, url=remoteUrl)\n", + "map4" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/zarr_sink_example.html b/notebooks/zarr_sink_example.html new file mode 100644 index 000000000..755d7f4c7 --- /dev/null +++ b/notebooks/zarr_sink_example.html @@ -0,0 +1,460 @@ + + + + + + + Using the Zarr Tile Sink — large_image documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Using the Zarr Tile Sink🔗

+

The ZarrFileTileSource class has file-writing capabilities; an empty image can be created, image data can be added as tiles or arbitrary regions, and the image can be saved to a file in any of several formats.

+

Typically, this class is called a “source” when reading from a file and a “sink” when writing to a file. This is just a naming convention, but the read mode and write mode are not mutually exclusive.

+
+

Installation🔗

+
+
[ ]:
+
+
+
# This will install large_image with the zarr source
+!pip install large_image[tiff,zarr,converter] --find-links https://girder.github.io/large_image_wheels
+
+# For maximum capabilities in Jupyter, also install ipyleaflet so you can
+# view zoomable images in the notebook
+!pip install ipyleaflet
+
+
+
+
+
[ ]:
+
+
+
# Ask JupyterLab to locally proxy an internal tile server
+import importlib.util
+import large_image
+
+if importlib.util.find_spec('google') and importlib.util.find_spec('google.colab'):
+    # colab intercepts localhost
+    large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = 'https://localhost'
+else:
+    large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = True
+
+
+
+
+
+

Sample Data Download🔗

+

For this example, we will use data from a sample file. We will copy and modify tiles from this image, writing the modified data to a new file.

+
+
[2]:
+
+
+
!curl -L -C - -o example.tiff https://demo.kitware.com/histomicstk/api/v1/item/58b480ba92ca9a000b08c899/download
+
+
+
+
+
+
+
+
+  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
+                                 Dload  Upload   Total   Spent    Left  Speed
+100 12.3M  100 12.3M    0     0  2952k      0  0:00:04  0:00:04 --:--:-- 2952k
+
+
+
+
[3]:
+
+
+
original_image_path = 'example.tiff'
+processed_image_path = 'processed_example_1.tiff'
+
+source = large_image.open(original_image_path)
+
+# view the metadata
+source_metadata = source.getMetadata()
+source_metadata
+
+
+
+
+
[3]:
+
+
+
+
+{'levels': 7,
+ 'sizeX': 9216,
+ 'sizeY': 11264,
+ 'tileWidth': 256,
+ 'tileHeight': 256,
+ 'magnification': 40.0,
+ 'mm_x': 0.00025,
+ 'mm_y': 0.00025,
+ 'dtype': 'uint8',
+ 'bandCount': 3}
+
+
+
+
[4]:
+
+
+
# show source as a static thumbnail
+source.getThumbnail()[0]
+
+
+
+
+
[4]:
+
+
+
+../_images/notebooks_zarr_sink_example_7_0.jpg +
+
+
+
[5]:
+
+
+
# show the source image in an interactive viewer
+source
+
+
+
+
+
+
+
+
+
+
+
+

Writing Processed Data to a New File🔗

+
+
[ ]:
+
+
+
from skimage.color.adapt_rgb import adapt_rgb, hsv_value
+from skimage import filters
+
+# define some image processing function
+
+@adapt_rgb(hsv_value)
+def process_tile(tile, footprint_size):
+    return filters.unsharp_mask(
+        tile, radius=footprint_size,
+    )
+
+
+
+
+
[7]:
+
+
+
# create a sink, which is an instance of ZarrFileTileSource and has no data
+sink = large_image.new()
+
+# compare three different footprint sizes for processing algorithm
+# computing the processed image takes about 1 minute for each value
+footprint_sizes = [1, 10, 50]
+print(f'Processing image for {len(footprint_sizes)} frames.')
+
+# create a frame for each processed result
+for i, footprint_size in enumerate(footprint_sizes):
+    print('Processing image with footprint_size = ', footprint_size)
+    # iterate through tiles, getting numpy arrays for each tile
+    for tile in source.tileIterator(format='numpy'):
+        # for each tile, run some processing algorithm
+        t = tile['tile']
+        processed_tile = process_tile(t, footprint_size) * 255
+
+        # add modified tile to sink
+        # specify tile x, tile y, and any arbitrary frame parameters
+        sink.addTile(processed_tile, x=tile['x'], y=tile['y'], i=i)
+# view metadata
+sink.getMetadata()
+
+
+
+
+
+
+
+
+Processing image for 3 frames.
+Processing image with footprint_size =  1
+Processing image with footprint_size =  10
+Processing image with footprint_size =  50
+
+
+
+
[7]:
+
+
+
+
+{'levels': 6,
+ 'sizeX': 9216,
+ 'sizeY': 11264,
+ 'tileWidth': 512,
+ 'tileHeight': 512,
+ 'magnification': None,
+ 'mm_x': 0,
+ 'mm_y': 0,
+ 'dtype': 'float64',
+ 'bandCount': 3,
+ 'frames': [{'Frame': 0, 'IndexI': 0, 'Index': 0, 'Channel': 'Band 1'},
+  {'Frame': 1, 'IndexI': 1, 'Index': 1, 'Channel': 'Band 1'},
+  {'Frame': 2, 'IndexI': 2, 'Index': 2, 'Channel': 'Band 1'}],
+ 'IndexRange': {'IndexI': 3},
+ 'IndexStride': {'IndexI': 1},
+ 'channels': ['Band 1'],
+ 'channelmap': {'Band 1': 0}}
+
+
+
+
[8]:
+
+
+
# show the result image in an interactive viewer
+# the viewer includes a slider for this multiframe image
+# switch between frames to view the differences between the values passed to footprint_size
+sink
+
+
+
+
+
+
+
+
+
+
+
+

Edit Attributes and Write Result File🔗

+
+
[9]:
+
+
+
# set crop bounds
+sink.crop = (3000, 5000, 2048, 2048)
+
+# set mm_x and mm_y from source metadata
+sink.mm_x = source_metadata.get('mm_x')
+sink.mm_y = source_metadata.get('mm_y')
+
+# set image description
+sink.imageDescription = 'processed with scikit-image'
+
+# add original thumbnail as an associated image
+sink.addAssociatedImage(source.getThumbnail()[0], imageKey='original')
+
+# write new image as tiff (other format options include .zip, .zarr, .db, .sqlite, .svs, etc.)
+sink.write(processed_image_path)
+
+
+
+
+
+

View Results🔗

+
+
[10]:
+
+
+
# open written file as a new source
+# this will be opened as a TiffFileTileSource
+source_2 = large_image.open(processed_image_path)
+
+# view metadata
+source_2.getMetadata()
+
+
+
+
+
[10]:
+
+
+
+
+{'levels': 4,
+ 'sizeX': 2048,
+ 'sizeY': 2048,
+ 'tileWidth': 256,
+ 'tileHeight': 256,
+ 'magnification': None,
+ 'mm_x': None,
+ 'mm_y': None,
+ 'dtype': 'uint16',
+ 'bandCount': 3,
+ 'frames': [{'Channel': 'Band 1', 'Frame': 0, 'Index': 0, 'IndexI': 0},
+  {'Channel': 'Band 1', 'Frame': 1, 'Index': 1, 'IndexI': 1},
+  {'Channel': 'Band 1', 'Frame': 2, 'Index': 2, 'IndexI': 2}],
+ 'IndexRange': {'IndexI': 3},
+ 'IndexStride': {'IndexI': 1},
+ 'channels': ['Band 1'],
+ 'channelmap': {'Band 1': 0}}
+
+
+
+
[11]:
+
+
+
# show source_2 as a static thumbnail
+source_2.getThumbnail()[0]
+
+
+
+
+
[11]:
+
+
+
+../_images/notebooks_zarr_sink_example_17_0.jpg +
+
+
+
[12]:
+
+
+
# show source_2 in an interactive viewer
+source_2
+
+
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/notebooks/zarr_sink_example.ipynb b/notebooks/zarr_sink_example.ipynb new file mode 100644 index 000000000..67ce180d0 --- /dev/null +++ b/notebooks/zarr_sink_example.ipynb @@ -0,0 +1,561 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9190dbbd", + "metadata": {}, + "source": [ + "# Using the Zarr Tile Sink\n", + "\n", + "The `ZarrFileTileSource` class has file-writing capabilities; an empty image can be created, image data can be added as tiles or arbitrary regions, and the image can be saved to a file in any of several formats.\n", + "\n", + "Typically, this class is called a \"source\" when reading from a file and a \"sink\" when writing to a file. This is just a naming convention, but the read mode and write mode are not mutually exclusive." + ] + }, + { + "cell_type": "markdown", + "id": "ebaa9c80", + "metadata": {}, + "source": [ + "## Installation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4ba28d02", + "metadata": {}, + "outputs": [], + "source": [ + "# This will install large_image with the zarr source\n", + "!pip install large_image[tiff,zarr,converter] --find-links https://girder.github.io/large_image_wheels\n", + "\n", + "# For maximum capabilities in Jupyter, also install ipyleaflet so you can\n", + "# view zoomable images in the notebook\n", + "!pip install ipyleaflet" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63c0c38f", + "metadata": {}, + "outputs": [], + "source": [ + "# Ask JupyterLab to locally proxy an internal tile server\n", + "import importlib.util\n", + "import large_image\n", + "\n", + "if importlib.util.find_spec('google') and importlib.util.find_spec('google.colab'):\n", + " # colab intercepts localhost\n", + " large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = 'https://localhost'\n", + "else:\n", + " large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY = True" + ] + }, + { + "cell_type": "markdown", + "id": "771078f9", + "metadata": {}, + "source": [ + "## Sample Data Download\n", + "\n", + "For this example, we will use data from a sample file. We will copy and modify tiles from this image, writing the modified data to a new file." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1c4b746b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " % Total % Received % Xferd Average Speed Time Time Time Current\n", + " Dload Upload Total Spent Left Speed\n", + "100 12.3M 100 12.3M 0 0 2952k 0 0:00:04 0:00:04 --:--:-- 2952k\n" + ] + } + ], + "source": [ + "!curl -L -C - -o example.tiff https://demo.kitware.com/histomicstk/api/v1/item/58b480ba92ca9a000b08c899/download" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5c39f222", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "bandCount": 3, + "dtype": "uint8", + "levels": 7, + "magnification": 40, + "mm_x": 0.00025, + "mm_y": 0.00025, + "sizeX": 9216, + "sizeY": 11264, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'levels': 7,\n", + " 'sizeX': 9216,\n", + " 'sizeY': 11264,\n", + " 'tileWidth': 256,\n", + " 'tileHeight': 256,\n", + " 'magnification': 40.0,\n", + " 'mm_x': 0.00025,\n", + " 'mm_y': 0.00025,\n", + " 'dtype': 'uint8',\n", + " 'bandCount': 3}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "original_image_path = 'example.tiff'\n", + "processed_image_path = 'processed_example_1.tiff'\n", + "\n", + "source = large_image.open(original_image_path)\n", + "\n", + "# view the metadata\n", + "source_metadata = source.getMetadata()\n", + "source_metadata" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d2d4956c", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "ImageBytes<12955> (image/jpeg)" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# show source as a static thumbnail\n", + "source.getThumbnail()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c018cebd", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bf6d0e9480cb43ef9851d7bd3ca7e356", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(Map(center=[5632.0, 4608.0], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_i…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# show the source image in an interactive viewer\n", + "source" + ] + }, + { + "cell_type": "markdown", + "id": "f88191b2", + "metadata": {}, + "source": [ + "## Writing Processed Data to a New File" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e75e6de", + "metadata": {}, + "outputs": [], + "source": [ + "from skimage.color.adapt_rgb import adapt_rgb, hsv_value\n", + "from skimage import filters\n", + "\n", + "# define some image processing function\n", + "\n", + "@adapt_rgb(hsv_value)\n", + "def process_tile(tile, footprint_size):\n", + " return filters.unsharp_mask(\n", + " tile, radius=footprint_size,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "fb89a0b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Processing image for 3 frames.\n", + "Processing image with footprint_size = 1\n", + "Processing image with footprint_size = 10\n", + "Processing image with footprint_size = 50\n" + ] + }, + { + "data": { + "application/json": { + "IndexRange": { + "IndexI": 3 + }, + "IndexStride": { + "IndexI": 1 + }, + "bandCount": 3, + "channelmap": { + "Band 1": 0 + }, + "channels": [ + "Band 1" + ], + "dtype": "float64", + "frames": [ + { + "Channel": "Band 1", + "Frame": 0, + "Index": 0, + "IndexI": 0 + }, + { + "Channel": "Band 1", + "Frame": 1, + "Index": 1, + "IndexI": 1 + }, + { + "Channel": "Band 1", + "Frame": 2, + "Index": 2, + "IndexI": 2 + } + ], + "levels": 6, + "magnification": null, + "mm_x": 0, + "mm_y": 0, + "sizeX": 9216, + "sizeY": 11264, + "tileHeight": 512, + "tileWidth": 512 + }, + "text/plain": [ + "{'levels': 6,\n", + " 'sizeX': 9216,\n", + " 'sizeY': 11264,\n", + " 'tileWidth': 512,\n", + " 'tileHeight': 512,\n", + " 'magnification': None,\n", + " 'mm_x': 0,\n", + " 'mm_y': 0,\n", + " 'dtype': 'float64',\n", + " 'bandCount': 3,\n", + " 'frames': [{'Frame': 0, 'IndexI': 0, 'Index': 0, 'Channel': 'Band 1'},\n", + " {'Frame': 1, 'IndexI': 1, 'Index': 1, 'Channel': 'Band 1'},\n", + " {'Frame': 2, 'IndexI': 2, 'Index': 2, 'Channel': 'Band 1'}],\n", + " 'IndexRange': {'IndexI': 3},\n", + " 'IndexStride': {'IndexI': 1},\n", + " 'channels': ['Band 1'],\n", + " 'channelmap': {'Band 1': 0}}" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# create a sink, which is an instance of ZarrFileTileSource and has no data\n", + "sink = large_image.new()\n", + "\n", + "# compare three different footprint sizes for processing algorithm\n", + "# computing the processed image takes about 1 minute for each value\n", + "footprint_sizes = [1, 10, 50]\n", + "print(f'Processing image for {len(footprint_sizes)} frames.')\n", + "\n", + "# create a frame for each processed result\n", + "for i, footprint_size in enumerate(footprint_sizes):\n", + " print('Processing image with footprint_size = ', footprint_size)\n", + " # iterate through tiles, getting numpy arrays for each tile\n", + " for tile in source.tileIterator(format='numpy'):\n", + " # for each tile, run some processing algorithm\n", + " t = tile['tile']\n", + " processed_tile = process_tile(t, footprint_size) * 255\n", + "\n", + " # add modified tile to sink\n", + " # specify tile x, tile y, and any arbitrary frame parameters\n", + " sink.addTile(processed_tile, x=tile['x'], y=tile['y'], i=i)\n", + "# view metadata\n", + "sink.getMetadata()" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "e9114da0", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9b8e6175005d4af89cb4cfada7b72983", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(IntSlider(value=0, description='Frame:', max=2), Map(center=[5632.0, 4608.0], controls=(ZoomCon…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# show the result image in an interactive viewer\n", + "# the viewer includes a slider for this multiframe image\n", + "# switch between frames to view the differences between the values passed to footprint_size\n", + "sink" + ] + }, + { + "cell_type": "markdown", + "id": "3c88d352", + "metadata": {}, + "source": [ + "## Edit Attributes and Write Result File" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "24e5c87c", + "metadata": {}, + "outputs": [], + "source": [ + "# set crop bounds\n", + "sink.crop = (3000, 5000, 2048, 2048)\n", + "\n", + "# set mm_x and mm_y from source metadata\n", + "sink.mm_x = source_metadata.get('mm_x')\n", + "sink.mm_y = source_metadata.get('mm_y')\n", + "\n", + "# set image description\n", + "sink.imageDescription = 'processed with scikit-image'\n", + "\n", + "# add original thumbnail as an associated image\n", + "sink.addAssociatedImage(source.getThumbnail()[0], imageKey='original')\n", + "\n", + "# write new image as tiff (other format options include .zip, .zarr, .db, .sqlite, .svs, etc.)\n", + "sink.write(processed_image_path)" + ] + }, + { + "cell_type": "markdown", + "id": "5ba5a838", + "metadata": {}, + "source": [ + "## View Results" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "46d62547", + "metadata": {}, + "outputs": [ + { + "data": { + "application/json": { + "IndexRange": { + "IndexI": 3 + }, + "IndexStride": { + "IndexI": 1 + }, + "bandCount": 3, + "channelmap": { + "Band 1": 0 + }, + "channels": [ + "Band 1" + ], + "dtype": "uint16", + "frames": [ + { + "Channel": "Band 1", + "Frame": 0, + "Index": 0, + "IndexI": 0 + }, + { + "Channel": "Band 1", + "Frame": 1, + "Index": 1, + "IndexI": 1 + }, + { + "Channel": "Band 1", + "Frame": 2, + "Index": 2, + "IndexI": 2 + } + ], + "levels": 4, + "magnification": null, + "mm_x": null, + "mm_y": null, + "sizeX": 2048, + "sizeY": 2048, + "tileHeight": 256, + "tileWidth": 256 + }, + "text/plain": [ + "{'levels': 4,\n", + " 'sizeX': 2048,\n", + " 'sizeY': 2048,\n", + " 'tileWidth': 256,\n", + " 'tileHeight': 256,\n", + " 'magnification': None,\n", + " 'mm_x': None,\n", + " 'mm_y': None,\n", + " 'dtype': 'uint16',\n", + " 'bandCount': 3,\n", + " 'frames': [{'Channel': 'Band 1', 'Frame': 0, 'Index': 0, 'IndexI': 0},\n", + " {'Channel': 'Band 1', 'Frame': 1, 'Index': 1, 'IndexI': 1},\n", + " {'Channel': 'Band 1', 'Frame': 2, 'Index': 2, 'IndexI': 2}],\n", + " 'IndexRange': {'IndexI': 3},\n", + " 'IndexStride': {'IndexI': 1},\n", + " 'channels': ['Band 1'],\n", + " 'channelmap': {'Band 1': 0}}" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# open written file as a new source\n", + "# this will be opened as a TiffFileTileSource\n", + "source_2 = large_image.open(processed_image_path)\n", + "\n", + "# view metadata\n", + "source_2.getMetadata()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "dbb8c86e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "text/plain": [ + "ImageBytes<56254> (image/jpeg)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# show source_2 as a static thumbnail\n", + "source_2.getThumbnail()[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d71bd65a", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "30812bb388a0426da9806e62bf5e8711", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(IntSlider(value=0, description='Frame:', max=2), Map(center=[1024.0, 1024.0], controls=(ZoomCon…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# show source_2 in an interactive viewer\n", + "source_2" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": false, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": {}, + "toc_section_display": true, + "toc_window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/objects.inv b/objects.inv new file mode 100644 index 000000000..60d880673 Binary files /dev/null and b/objects.inv differ diff --git a/plottable.html b/plottable.html new file mode 100644 index 000000000..fa7d00777 --- /dev/null +++ b/plottable.html @@ -0,0 +1,223 @@ + + + + + + + Plottable Data — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Plottable Data🔗

+

There is a python utility class and some REST endpoints to obtain plottable data related to a large image item optionally along with other items in the same Girder folder. This plottable data can be drawn from item metadata, folder metadata, annotation attributes, and annotation element user properties, plus a few core properties of items, annotations, and annotation elements.

+

See the PlottableItemData for the Python API, or POST annotation/item/{id}/plot/list and POST annotation/item/{id}/plot/data for the endpoints.

+

In general, data is parsed from the meta dictionary of items and folders, from the attributes dictionary of annotations, and the user dictionary of annotation elements. For any of these locations, any data that has a data type of a string, number (floating point or integer), or boolean can be used. Data must be referenced by a dictionary key, and can be in nested dictionaries and arrays, but not recursively.

+

Structurally, data can be referenced within the primary dictionary of each resource via any of the following access patterns (where data is the primary dictionary: meta, attributes, or user):

+

For instance, each of the following has two separate keys with plottable data (some with one value and some with an array of three values):

+ + + + + + + + + + + + + + + + + + + + + + + +

Data Arrangement

Example

key - value

"meta": {
+  "nucleus_radius": 4.5,
+  "nucleus_circularity": 0.9
+}
+
+
+

key - value list

"meta": {
+  "nucleus_radius": [4.5, 5.5, 5.1],
+  "nucleus_circularity": [0.9, 0.86, 0.92]
+}
+
+
+

nested key - value

"meta": {
+  "nucleus": {
+    "radius": 4.5,
+    "circularity": 0.9
+  }
+}
+
+
+

nested key - value list

"meta": {
+  "nucleus": {
+    "radius": [4.5, 5.5, 5.1],
+    "circularity": [0.9, 0.86, 0.92]
+  }
+}
+
+
+

key - list of key - value

"meta": {
+  "nucleus": [
+    {
+      "radius": 4.5,
+      "circularity": 0.9
+    },
+    {
+      "radius": 5.5,
+      "circularity": 0.86
+    },
+    {
+      "radius": 5.1,
+      "circularity": 0.92
+    }
+  ]
+}
+
+
+
+

For plottable data in the meta key of a folder, (a) if the name of the most nested key matches the Python regular expression r'(?i)(item|image)_(id|name)$', then the value of that key will attempt to be resolved to the name or Girder _id of a item within the folder. This will only match items that are included (the adjacentItems flag affects the results). If a matching item is found, the item’s canonical name and _id are used rather than the specified value. (b) if the name of the key matches the Python regular expression r'(?i)(low|min|high|max)(_|)(x|y)', the value is assumed to be a coordinate of an image bounding box. This can be used by clients to show a portion of the image associated with the plottable record.

+

After plottable data is gathered, columns of available data are summarized. Each column contains:

+
    +
  • title: a display-friendly title for the column.

  • +
  • key: the name of column for internal purposes and requests.

  • +
  • type: either 'number' if all of the examined values for the data are None or can be cast to a float or 'string' otherwise.

  • +
  • count: the number of rows where the value of this column is not None.

  • +
  • distinct: a list of distinct values if there are less than some maximum number of distinct values.

  • +
  • distinctcount: if distinct is present, the number of values in that list.

  • +
  • min: for number data columns, the lowest value present.

  • +
  • max: for number data columns, the highest value present.

  • +
+

Data rows are populated with more general data when possible. For example, if data is selected from annotations, metadata from the parent item is present in each row.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/py-modindex.html b/py-modindex.html new file mode 100644 index 000000000..caf7b28ea --- /dev/null +++ b/py-modindex.html @@ -0,0 +1,581 @@ + + + + + + Python Module Index — large_image documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Python Module Index

+ +
+ g | + l +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ g
+ girder_large_image +
    + girder_large_image.constants +
    + girder_large_image.girder_tilesource +
    + girder_large_image.loadmodelcache +
    + girder_large_image.models +
    + girder_large_image.models.image_item +
    + girder_large_image.rest +
    + girder_large_image.rest.item_meta +
    + girder_large_image.rest.large_image_resource +
    + girder_large_image.rest.tiles +
+ girder_large_image_annotation +
    + girder_large_image_annotation.constants +
    + girder_large_image_annotation.handlers +
    + girder_large_image_annotation.models +
    + girder_large_image_annotation.models.annotation +
    + girder_large_image_annotation.models.annotationelement +
    + girder_large_image_annotation.rest +
    + girder_large_image_annotation.rest.annotation +
    + girder_large_image_annotation.utils +
 
+ l
+ large_image +
    + large_image.cache_util +
    + large_image.cache_util.base +
    + large_image.cache_util.cache +
    + large_image.cache_util.cachefactory +
    + large_image.cache_util.memcache +
    + large_image.cache_util.rediscache +
    + large_image.config +
    + large_image.constants +
    + large_image.exceptions +
    + large_image.tilesource +
    + large_image.tilesource.base +
    + large_image.tilesource.geo +
    + large_image.tilesource.jupyter +
    + large_image.tilesource.resample +
    + large_image.tilesource.stylefuncs +
    + large_image.tilesource.tiledict +
    + large_image.tilesource.tileiterator +
    + large_image.tilesource.utilities +
+ large_image_converter +
    + large_image_converter.format_aperio +
+ large_image_source_bioformats +
    + large_image_source_bioformats.girder_source +
+ large_image_source_deepzoom +
    + large_image_source_deepzoom.girder_source +
+ large_image_source_dicom +
    + large_image_source_dicom.assetstore +
    + large_image_source_dicom.assetstore.dicomweb_assetstore_adapter +
    + large_image_source_dicom.assetstore.rest +
    + large_image_source_dicom.dicom_metadata +
    + large_image_source_dicom.dicom_tags +
    + large_image_source_dicom.dicomweb_utils +
    + large_image_source_dicom.girder_plugin +
    + large_image_source_dicom.girder_source +
+ large_image_source_dummy +
+ large_image_source_gdal +
    + large_image_source_gdal.girder_source +
+ large_image_source_mapnik +
    + large_image_source_mapnik.girder_source +
+ large_image_source_multi +
    + large_image_source_multi.girder_source +
+ large_image_source_nd2 +
    + large_image_source_nd2.girder_source +
+ large_image_source_ometiff +
    + large_image_source_ometiff.girder_source +
+ large_image_source_openjpeg +
    + large_image_source_openjpeg.girder_source +
+ large_image_source_openslide +
    + large_image_source_openslide.girder_source +
+ large_image_source_pil +
    + large_image_source_pil.girder_source +
+ large_image_source_rasterio +
    + large_image_source_rasterio.girder_source +
+ large_image_source_test +
+ large_image_source_tiff +
    + large_image_source_tiff.exceptions +
    + large_image_source_tiff.girder_source +
    + large_image_source_tiff.tiff_reader +
+ large_image_source_tifffile +
    + large_image_source_tifffile.girder_source +
+ large_image_source_vips +
    + large_image_source_vips.girder_source +
+ large_image_source_zarr +
    + large_image_source_zarr.girder_source +
+ large_image_tasks +
    + large_image_tasks.tasks +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/search.html b/search.html new file mode 100644 index 000000000..101289015 --- /dev/null +++ b/search.html @@ -0,0 +1,137 @@ + + + + + + Search — large_image documentation + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 000000000..7c9195ebf --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {".large_image_config.yaml": [[62, "large-image-config-yaml"], [64, "large-image-config-yaml"]], "A sample annotation": [[54, "a-sample-annotation"]], "API Documentation": [[55, null]], "All Vector Shapes": [[54, "all-vector-shapes"]], "All shapes": [[54, "all-shapes"]], "Annotation Schema": [[54, null]], "Apply a gamma correction to the image": [[73, "apply-a-gamma-correction-to-the-image"]], "Arrow": [[54, "arrow"]], "Associated Images": [[61, "associated-images"]], "Basic Use": [[70, "Basic-Use"]], "Caching Large Image in Girder": [[63, null]], "Caching in Large Image": [[56, null]], "Channels, Bands, Samples, and Axes": [[61, "channels-bands-samples-and-axes"]], "Circle": [[54, "circle"]], "Colors": [[54, "colors"]], "Command Line Usage": [[66, "command-line-usage"]], "Component Values": [[54, "component-values"]], "Composite To A Single Frame": [[68, "composite-to-a-single-frame"]], "Composite With Scaling": [[68, "composite-with-scaling"]], "Composite several frames with framedelta": [[73, "composite-several-frames-with-framedelta"]], "Conda": [[61, "conda"], [67, "conda"]], "Configuration Options": [[57, null]], "Configuration Parameters": [[57, "id1"]], "Configuration from Environment": [[57, "configuration-from-environment"]], "Configuration from Python": [[57, "configuration-from-python"]], "Configuration within the Girder Plugin": [[57, "configuration-within-the-girder-plugin"]], "Contents:": [[55, null], [65, null], [67, null]], "Coordinates": [[54, "coordinates"]], "DICOMweb Assetstore": [[59, null]], "Developer Guide": [[58, null]], "Developer Installation": [[58, "developer-installation"]], "Development Environment": [[58, "development-environment"]], "Docker Image": [[67, "docker-image"]], "Edges": [[73, "edges"]], "Edit Attributes and Write Result File": [[71, "Edit-Attributes-and-Write-Result-File"]], "Editing Configuration Files": [[64, "editing-configuration-files"]], "Elements": [[54, "elements"]], "Ellipse": [[54, "ellipse"]], "Encoding": [[73, "encoding"]], "Examples": [[68, "examples"], [73, "examples"]], "Extensions": [[60, "extensions"]], "Fill missing data and apply categorical colormap": [[73, "fill-missing-data-and-apply-categorical-colormap"]], "Format": [[73, "format"]], "Full Schema": [[54, "full-schema"], [68, "full-schema"]], "General Plugin Settings": [[62, "general-plugin-settings"], [64, "general-plugin-settings"]], "Geospatial Sources": [[70, "Geospatial-Sources"]], "Getting Started": [[61, null]], "Getting a Region of an Image": [[61, "getting-a-region-of-an-image"]], "Getting a Thumbnail": [[61, "getting-a-thumbnail"]], "Girder Annotation Configuration Options": [[62, null]], "Girder Configuration Options": [[64, null]], "Girder Integration": [[65, null]], "Girder Server Sources": [[70, "Girder-Server-Sources"]], "Grid Data": [[54, "grid-data"]], "Heatmap": [[54, "heatmap"]], "Highlights": [[67, "highlights"]], "Image Conversion": [[66, null]], "Image Formats": [[60, null]], "Image Frame Preset Defaults": [[64, "image-frame-preset-defaults"]], "Image Frame Presets": [[64, "image-frame-presets"]], "Image overlays": [[54, "image-overlays"]], "Images with Multiple Frames": [[61, "images-with-multiple-frames"]], "Indices and tables": [[67, "indices-and-tables"]], "Installation": [[61, "installation"], [67, "installation"], [70, "Installation"], [71, "Installation"]], "Item Lists": [[64, "item-lists"]], "Item Metadata": [[64, "item-metadata"]], "Iterating Across an Image": [[61, "iterating-across-an-image"]], "Jupyter Notebook Examples": [[69, null]], "Large Image": [[67, null]], "Logging from Python": [[57, "logging-from-python"]], "Migration from Girder 2 to Girder 3": [[74, "migration-from-girder-2-to-girder-3"]], "Mime Types": [[60, "mime-types"]], "Module contents": [[0, "module-girder_large_image"], [1, "module-girder_large_image.models"], [2, "module-girder_large_image.rest"], [4, "module-girder_large_image_annotation"], [5, "module-girder_large_image_annotation.models"], [6, "module-girder_large_image_annotation.rest"], [7, "module-girder_large_image_annotation.utils"], [9, "module-large_image"], [10, "module-large_image.cache_util"], [11, "module-large_image.tilesource"], [13, "module-large_image_converter"], [15, "module-large_image_source_bioformats"], [17, "module-large_image_source_deepzoom"], [19, "module-large_image_source_dicom"], [20, "module-large_image_source_dicom.assetstore"], [22, "module-large_image_source_dummy"], [24, "module-large_image_source_gdal"], [26, "module-large_image_source_mapnik"], [28, "module-large_image_source_multi"], [30, "module-large_image_source_nd2"], [32, "module-large_image_source_ometiff"], [34, "module-large_image_source_openjpeg"], [36, "module-large_image_source_openslide"], [38, "module-large_image_source_pil"], [40, "module-large_image_source_rasterio"], [42, "module-large_image_source_test"], [44, "module-large_image_source_tiff"], [46, "module-large_image_source_tifffile"], [48, "module-large_image_source_vips"], [50, "module-large_image_source_zarr"], [52, "module-large_image_tasks"]], "Modules": [[67, "modules"]], "Mongo for Girder Tests or Development": [[58, "mongo-for-girder-tests-or-development"]], "Multi Source Schema": [[68, null]], "Multi Z-position": [[68, "multi-z-position"]], "Pip": [[61, "pip"], [67, "pip"]], "Plottable Data": [[72, null]], "Point": [[54, "point"]], "Polyline": [[54, "polyline"]], "Preferred Extensions and Mime Types": [[60, "preferred-extensions-and-mime-types"]], "Projections": [[61, "projections"]], "Python Usage": [[66, "python-usage"]], "Reading Image Metadata": [[61, "reading-image-metadata"]], "Rectangle": [[54, "rectangle"]], "Rectangle Grid": [[54, "rectangle-grid"]], "Running Tests": [[58, "running-tests"]], "Sample Data Download": [[71, "Sample-Data-Download"]], "Store annotation history": [[62, "store-annotation-history"]], "Style": [[73, "style"]], "Styles - Changing colors, scales, and other properties": [[61, "styles-changing-colors-scales-and-other-properties"]], "Submodules": [[0, "submodules"], [1, "submodules"], [2, "submodules"], [4, "submodules"], [5, "submodules"], [6, "submodules"], [9, "submodules"], [10, "submodules"], [11, "submodules"], [13, "submodules"], [15, "submodules"], [17, "submodules"], [19, "submodules"], [20, "submodules"], [24, "submodules"], [26, "submodules"], [28, "submodules"], [30, "submodules"], [32, "submodules"], [34, "submodules"], [36, "submodules"], [38, "submodules"], [40, "submodules"], [44, "submodules"], [46, "submodules"], [48, "submodules"], [50, "submodules"], [52, "submodules"]], "Subpackages": [[0, "subpackages"], [4, "subpackages"], [9, "subpackages"], [19, "subpackages"]], "Swap the red and green channels of a three color image": [[73, "swap-the-red-and-green-channels-of-a-three-color-image"]], "Test Requirements": [[58, "test-requirements"]], "Tile Serving": [[61, "tile-serving"]], "Tile Source Options": [[73, null]], "Tile Source Requirements": [[58, "tile-source-requirements"]], "Tiled pixelmap overlays": [[54, "tiled-pixelmap-overlays"]], "Upgrading from Previous Versions": [[74, null]], "Using Large Image in Jupyter": [[70, null]], "Using Local Files": [[70, "Using-Local-Files"]], "Using the Zarr Tile Sink": [[71, null]], "View Results": [[71, "View-Results"]], "Writing Processed Data to a New File": [[71, "Writing-Processed-Data-to-a-New-File"]], "Writing an Image": [[61, "writing-an-image"]], "YAML Configuration Files": [[64, "yaml-configuration-files"]], "girder_large_image": [[3, null]], "girder_large_image package": [[0, null]], "girder_large_image.constants module": [[0, "module-girder_large_image.constants"]], "girder_large_image.girder_tilesource module": [[0, "module-girder_large_image.girder_tilesource"]], "girder_large_image.loadmodelcache module": [[0, "module-girder_large_image.loadmodelcache"]], "girder_large_image.models package": [[1, null]], "girder_large_image.models.image_item module": [[1, "module-girder_large_image.models.image_item"]], "girder_large_image.rest package": [[2, null]], "girder_large_image.rest.item_meta module": [[2, "module-girder_large_image.rest.item_meta"]], "girder_large_image.rest.large_image_resource module": [[2, "module-girder_large_image.rest.large_image_resource"]], "girder_large_image.rest.tiles module": [[2, "module-girder_large_image.rest.tiles"]], "girder_large_image_annotation": [[8, null]], "girder_large_image_annotation package": [[4, null]], "girder_large_image_annotation.constants module": [[4, "module-girder_large_image_annotation.constants"]], "girder_large_image_annotation.handlers module": [[4, "module-girder_large_image_annotation.handlers"]], "girder_large_image_annotation.models package": [[5, null]], "girder_large_image_annotation.models.annotation module": [[5, "module-girder_large_image_annotation.models.annotation"]], "girder_large_image_annotation.models.annotationelement module": [[5, "module-girder_large_image_annotation.models.annotationelement"]], "girder_large_image_annotation.rest package": [[6, null]], "girder_large_image_annotation.rest.annotation module": [[6, "module-girder_large_image_annotation.rest.annotation"]], "girder_large_image_annotation.utils package": [[7, null]], "large_image": [[12, null]], "large_image package": [[9, null]], "large_image.cache_util package": [[10, null]], "large_image.cache_util.base module": [[10, "module-large_image.cache_util.base"]], "large_image.cache_util.cache module": [[10, "module-large_image.cache_util.cache"]], "large_image.cache_util.cachefactory module": [[10, "module-large_image.cache_util.cachefactory"]], "large_image.cache_util.memcache module": [[10, "module-large_image.cache_util.memcache"]], "large_image.cache_util.rediscache module": [[10, "module-large_image.cache_util.rediscache"]], "large_image.config module": [[9, "module-large_image.config"]], "large_image.constants module": [[9, "module-large_image.constants"]], "large_image.exceptions module": [[9, "module-large_image.exceptions"]], "large_image.tilesource package": [[11, null]], "large_image.tilesource.base module": [[11, "module-large_image.tilesource.base"]], "large_image.tilesource.geo module": [[11, "module-large_image.tilesource.geo"]], "large_image.tilesource.jupyter module": [[11, "module-large_image.tilesource.jupyter"]], "large_image.tilesource.resample module": [[11, "module-large_image.tilesource.resample"]], "large_image.tilesource.stylefuncs module": [[11, "module-large_image.tilesource.stylefuncs"]], "large_image.tilesource.tiledict module": [[11, "module-large_image.tilesource.tiledict"]], "large_image.tilesource.tileiterator module": [[11, "module-large_image.tilesource.tileiterator"]], "large_image.tilesource.utilities module": [[11, "module-large_image.tilesource.utilities"]], "large_image_converter": [[14, null]], "large_image_converter package": [[13, null]], "large_image_converter.format_aperio module": [[13, "module-large_image_converter.format_aperio"]], "large_image_source_bioformats": [[16, null]], "large_image_source_bioformats package": [[15, null]], "large_image_source_bioformats.girder_source module": [[15, "module-large_image_source_bioformats.girder_source"]], "large_image_source_deepzoom": [[18, null]], "large_image_source_deepzoom package": [[17, null]], "large_image_source_deepzoom.girder_source module": [[17, "module-large_image_source_deepzoom.girder_source"]], "large_image_source_dicom": [[21, null]], "large_image_source_dicom package": [[19, null]], "large_image_source_dicom.assetstore package": [[20, null]], "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter module": [[20, "module-large_image_source_dicom.assetstore.dicomweb_assetstore_adapter"]], "large_image_source_dicom.assetstore.rest module": [[20, "module-large_image_source_dicom.assetstore.rest"]], "large_image_source_dicom.dicom_metadata module": [[19, "module-large_image_source_dicom.dicom_metadata"]], "large_image_source_dicom.dicom_tags module": [[19, "module-large_image_source_dicom.dicom_tags"]], "large_image_source_dicom.dicomweb_utils module": [[19, "module-large_image_source_dicom.dicomweb_utils"]], "large_image_source_dicom.girder_plugin module": [[19, "module-large_image_source_dicom.girder_plugin"]], "large_image_source_dicom.girder_source module": [[19, "module-large_image_source_dicom.girder_source"]], "large_image_source_dummy": [[23, null]], "large_image_source_dummy package": [[22, null]], "large_image_source_gdal": [[25, null]], "large_image_source_gdal package": [[24, null]], "large_image_source_gdal.girder_source module": [[24, "module-large_image_source_gdal.girder_source"]], "large_image_source_mapnik": [[27, null]], "large_image_source_mapnik package": [[26, null]], "large_image_source_mapnik.girder_source module": [[26, "module-large_image_source_mapnik.girder_source"]], "large_image_source_multi": [[29, null]], "large_image_source_multi package": [[28, null]], "large_image_source_multi.girder_source module": [[28, "module-large_image_source_multi.girder_source"]], "large_image_source_nd2": [[31, null]], "large_image_source_nd2 package": [[30, null]], "large_image_source_nd2.girder_source module": [[30, "module-large_image_source_nd2.girder_source"]], "large_image_source_ometiff": [[33, null]], "large_image_source_ometiff package": [[32, null]], "large_image_source_ometiff.girder_source module": [[32, "module-large_image_source_ometiff.girder_source"]], "large_image_source_openjpeg": [[35, null]], "large_image_source_openjpeg package": [[34, null]], "large_image_source_openjpeg.girder_source module": [[34, "module-large_image_source_openjpeg.girder_source"]], "large_image_source_openslide": [[37, null]], "large_image_source_openslide package": [[36, null]], "large_image_source_openslide.girder_source module": [[36, "module-large_image_source_openslide.girder_source"]], "large_image_source_pil": [[39, null]], "large_image_source_pil package": [[38, null]], "large_image_source_pil.girder_source module": [[38, "module-large_image_source_pil.girder_source"]], "large_image_source_rasterio": [[41, null]], "large_image_source_rasterio package": [[40, null]], "large_image_source_rasterio.girder_source module": [[40, "module-large_image_source_rasterio.girder_source"]], "large_image_source_test": [[43, null]], "large_image_source_test package": [[42, null]], "large_image_source_tiff": [[45, null]], "large_image_source_tiff package": [[44, null]], "large_image_source_tiff.exceptions module": [[44, "module-large_image_source_tiff.exceptions"]], "large_image_source_tiff.girder_source module": [[44, "module-large_image_source_tiff.girder_source"]], "large_image_source_tiff.tiff_reader module": [[44, "module-large_image_source_tiff.tiff_reader"]], "large_image_source_tifffile": [[47, null]], "large_image_source_tifffile package": [[46, null]], "large_image_source_tifffile.girder_source module": [[46, "module-large_image_source_tifffile.girder_source"]], "large_image_source_vips": [[49, null]], "large_image_source_vips package": [[48, null]], "large_image_source_vips.girder_source module": [[48, "module-large_image_source_vips.girder_source"]], "large_image_source_zarr": [[51, null]], "large_image_source_zarr package": [[50, null]], "large_image_source_zarr.girder_source module": [[50, "module-large_image_source_zarr.girder_source"]], "large_image_tasks": [[53, null]], "large_image_tasks package": [[52, null]], "large_image_tasks.tasks module": [[52, "module-large_image_tasks.tasks"]], "nodejs and npm for Girder Tests or Development": [[58, "nodejs-and-npm-for-girder-tests-or-development"]]}, "docnames": ["_build/girder_large_image/girder_large_image", "_build/girder_large_image/girder_large_image.models", "_build/girder_large_image/girder_large_image.rest", "_build/girder_large_image/modules", "_build/girder_large_image_annotation/girder_large_image_annotation", "_build/girder_large_image_annotation/girder_large_image_annotation.models", "_build/girder_large_image_annotation/girder_large_image_annotation.rest", "_build/girder_large_image_annotation/girder_large_image_annotation.utils", "_build/girder_large_image_annotation/modules", "_build/large_image/large_image", "_build/large_image/large_image.cache_util", "_build/large_image/large_image.tilesource", "_build/large_image/modules", "_build/large_image_converter/large_image_converter", "_build/large_image_converter/modules", "_build/large_image_source_bioformats/large_image_source_bioformats", "_build/large_image_source_bioformats/modules", "_build/large_image_source_deepzoom/large_image_source_deepzoom", "_build/large_image_source_deepzoom/modules", "_build/large_image_source_dicom/large_image_source_dicom", "_build/large_image_source_dicom/large_image_source_dicom.assetstore", "_build/large_image_source_dicom/modules", "_build/large_image_source_dummy/large_image_source_dummy", "_build/large_image_source_dummy/modules", "_build/large_image_source_gdal/large_image_source_gdal", "_build/large_image_source_gdal/modules", "_build/large_image_source_mapnik/large_image_source_mapnik", "_build/large_image_source_mapnik/modules", "_build/large_image_source_multi/large_image_source_multi", "_build/large_image_source_multi/modules", "_build/large_image_source_nd2/large_image_source_nd2", "_build/large_image_source_nd2/modules", "_build/large_image_source_ometiff/large_image_source_ometiff", "_build/large_image_source_ometiff/modules", "_build/large_image_source_openjpeg/large_image_source_openjpeg", "_build/large_image_source_openjpeg/modules", "_build/large_image_source_openslide/large_image_source_openslide", "_build/large_image_source_openslide/modules", "_build/large_image_source_pil/large_image_source_pil", "_build/large_image_source_pil/modules", "_build/large_image_source_rasterio/large_image_source_rasterio", "_build/large_image_source_rasterio/modules", "_build/large_image_source_test/large_image_source_test", "_build/large_image_source_test/modules", "_build/large_image_source_tiff/large_image_source_tiff", "_build/large_image_source_tiff/modules", "_build/large_image_source_tifffile/large_image_source_tifffile", "_build/large_image_source_tifffile/modules", "_build/large_image_source_vips/large_image_source_vips", "_build/large_image_source_vips/modules", "_build/large_image_source_zarr/large_image_source_zarr", "_build/large_image_source_zarr/modules", "_build/large_image_tasks/large_image_tasks", "_build/large_image_tasks/modules", "annotations", "api_index", "caching", "config_options", "development", "dicomweb_assetstore", "formats", "getting_started", "girder_annotation_config_options", "girder_caching", "girder_config_options", "girder_index", "image_conversion", "index", "multi_source_specification", "notebooks", "notebooks/large_image_examples", "notebooks/zarr_sink_example", "plottable", "tilesource_options", "upgrade"], "envversion": {"nbsphinx": 4, "sphinx": 62, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.intersphinx": 1, "sphinx.ext.todo": 2, "sphinx.ext.viewcode": 1}, "filenames": ["_build/girder_large_image/girder_large_image.rst", "_build/girder_large_image/girder_large_image.models.rst", "_build/girder_large_image/girder_large_image.rest.rst", "_build/girder_large_image/modules.rst", "_build/girder_large_image_annotation/girder_large_image_annotation.rst", "_build/girder_large_image_annotation/girder_large_image_annotation.models.rst", "_build/girder_large_image_annotation/girder_large_image_annotation.rest.rst", "_build/girder_large_image_annotation/girder_large_image_annotation.utils.rst", "_build/girder_large_image_annotation/modules.rst", "_build/large_image/large_image.rst", "_build/large_image/large_image.cache_util.rst", "_build/large_image/large_image.tilesource.rst", "_build/large_image/modules.rst", "_build/large_image_converter/large_image_converter.rst", "_build/large_image_converter/modules.rst", "_build/large_image_source_bioformats/large_image_source_bioformats.rst", "_build/large_image_source_bioformats/modules.rst", "_build/large_image_source_deepzoom/large_image_source_deepzoom.rst", "_build/large_image_source_deepzoom/modules.rst", "_build/large_image_source_dicom/large_image_source_dicom.rst", "_build/large_image_source_dicom/large_image_source_dicom.assetstore.rst", "_build/large_image_source_dicom/modules.rst", "_build/large_image_source_dummy/large_image_source_dummy.rst", "_build/large_image_source_dummy/modules.rst", "_build/large_image_source_gdal/large_image_source_gdal.rst", "_build/large_image_source_gdal/modules.rst", "_build/large_image_source_mapnik/large_image_source_mapnik.rst", "_build/large_image_source_mapnik/modules.rst", "_build/large_image_source_multi/large_image_source_multi.rst", "_build/large_image_source_multi/modules.rst", "_build/large_image_source_nd2/large_image_source_nd2.rst", "_build/large_image_source_nd2/modules.rst", "_build/large_image_source_ometiff/large_image_source_ometiff.rst", "_build/large_image_source_ometiff/modules.rst", "_build/large_image_source_openjpeg/large_image_source_openjpeg.rst", "_build/large_image_source_openjpeg/modules.rst", "_build/large_image_source_openslide/large_image_source_openslide.rst", "_build/large_image_source_openslide/modules.rst", "_build/large_image_source_pil/large_image_source_pil.rst", "_build/large_image_source_pil/modules.rst", "_build/large_image_source_rasterio/large_image_source_rasterio.rst", "_build/large_image_source_rasterio/modules.rst", "_build/large_image_source_test/large_image_source_test.rst", "_build/large_image_source_test/modules.rst", "_build/large_image_source_tiff/large_image_source_tiff.rst", "_build/large_image_source_tiff/modules.rst", "_build/large_image_source_tifffile/large_image_source_tifffile.rst", "_build/large_image_source_tifffile/modules.rst", "_build/large_image_source_vips/large_image_source_vips.rst", "_build/large_image_source_vips/modules.rst", "_build/large_image_source_zarr/large_image_source_zarr.rst", "_build/large_image_source_zarr/modules.rst", "_build/large_image_tasks/large_image_tasks.rst", "_build/large_image_tasks/modules.rst", "annotations.rst", "api_index.rst", "caching.rst", "config_options.rst", "development.rst", "dicomweb_assetstore.rst", "formats.rst", "getting_started.rst", "girder_annotation_config_options.rst", "girder_caching.rst", "girder_config_options.rst", "girder_index.rst", "image_conversion.rst", "index.rst", "multi_source_specification.rst", "notebooks.rst", "notebooks/large_image_examples.ipynb", "notebooks/zarr_sink_example.ipynb", "plottable.rst", "tilesource_options.rst", "upgrade.rst"], "indexentries": {"addassociatedimage() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.addAssociatedImage", false]], "addknownextensions() (large_image_source_bioformats.bioformatsfiletilesource class method)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.addKnownExtensions", false]], "addknownextensions() (large_image_source_gdal.gdalfiletilesource class method)": [[24, "large_image_source_gdal.GDALFileTileSource.addKnownExtensions", false]], "addknownextensions() (large_image_source_pil.pilfiletilesource class method)": [[38, "large_image_source_pil.PILFileTileSource.addKnownExtensions", false]], "addknownextensions() (large_image_source_rasterio.rasteriofiletilesource class method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.addKnownExtensions", false]], "addknownextensions() (large_image_source_tifffile.tifffilefiletilesource class method)": [[46, "large_image_source_tifffile.TifffileFileTileSource.addKnownExtensions", false]], "addknownextensions() (large_image_source_vips.vipsfiletilesource class method)": [[48, "large_image_source_vips.VipsFileTileSource.addKnownExtensions", false]], "addpilformatstooutputoptions() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.addPILFormatsToOutputOptions", false]], "addstyle() (large_image_source_mapnik.mapnikfiletilesource method)": [[26, "large_image_source_mapnik.MapnikFileTileSource.addStyle", false]], "addsystemendpoints() (in module girder_large_image.rest)": [[2, "girder_large_image.rest.addSystemEndpoints", false]], "addtile() (large_image_source_vips.vipsfiletilesource method)": [[48, "large_image_source_vips.VipsFileTileSource.addTile", false]], "addtile() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.addTile", false]], "addtilesthumbnails() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.addTilesThumbnails", false]], "adjust_params() (in module large_image_converter.format_aperio)": [[13, "large_image_converter.format_aperio.adjust_params", false]], "adjustconfigforuser() (in module girder_large_image)": [[0, "girder_large_image.adjustConfigForUser", false]], "allowedtypes (girder_large_image_annotation.utils.plottableitemdata attribute)": [[7, "girder_large_image_annotation.utils.PlottableItemData.allowedTypes", false]], "annotation (class in girder_large_image_annotation.models.annotation)": [[5, "girder_large_image_annotation.models.annotation.Annotation", false]], "annotation (girder_large_image_annotation.utils.geojsonannotation property)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.annotation", false]], "annotation.skill (class in girder_large_image_annotation.models.annotation)": [[5, "girder_large_image_annotation.models.annotation.Annotation.Skill", false]], "annotationelement (class in girder_large_image_annotation.models.annotationelement)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement", false]], "annotationelementschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.annotationElementSchema", false]], "annotationgeojson (class in girder_large_image_annotation.utils)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON", false]], "annotationresource (class in girder_large_image_annotation.rest.annotation)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource", false]], "annotationschema (class in girder_large_image_annotation.models.annotation)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema", false]], "annotationschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.annotationSchema", false]], "annotationtojson() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.annotationToJSON", false]], "arrowshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.arrowShapeSchema", false]], "as_leaflet_layer() (large_image.tilesource.jupyter.ipyleafletmixin method)": [[11, "large_image.tilesource.jupyter.IPyLeafletMixin.as_leaflet_layer", false]], "assetstore_meta (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter property)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.assetstore_meta", false]], "assetstore_meta (large_image_source_dicom.assetstore.dicomwebassetstoreadapter property)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.assetstore_meta", false]], "auth_session (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter property)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.auth_session", false]], "auth_session (large_image_source_dicom.assetstore.dicomwebassetstoreadapter property)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.auth_session", false]], "bandcount (large_image.tilesource.base.tilesource property)": [[11, "large_image.tilesource.base.TileSource.bandCount", false]], "bandcount (large_image.tilesource.tilesource property)": [[11, "large_image.tilesource.TileSource.bandCount", false]], "bandformat (large_image_source_vips.vipsfiletilesource property)": [[48, "large_image_source_vips.VipsFileTileSource.bandFormat", false]], "bandranges (large_image_source_vips.vipsfiletilesource property)": [[48, "large_image_source_vips.VipsFileTileSource.bandRanges", false]], "basecache (class in large_image.cache_util.base)": [[10, "large_image.cache_util.base.BaseCache", false]], "baseelementschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.baseElementSchema", false]], "basefields (girder_large_image_annotation.models.annotation.annotation attribute)": [[5, "girder_large_image_annotation.models.annotation.Annotation.baseFields", false]], "baserectangleshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.baseRectangleShapeSchema", false]], "baseshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.baseShapeSchema", false]], "bboxkeys (girder_large_image_annotation.models.annotationelement.annotationelement attribute)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.bboxKeys", false]], "bioformatsfiletilesource (class in large_image_source_bioformats)": [[15, "large_image_source_bioformats.BioformatsFileTileSource", false]], "bioformatsgirdertilesource (class in large_image_source_bioformats.girder_source)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource", false]], "cache_histograms_job() (in module large_image_tasks.tasks)": [[52, "large_image_tasks.tasks.cache_histograms_job", false]], "cache_tile_frames_job() (in module large_image_tasks.tasks)": [[52, "large_image_tasks.tasks.cache_tile_frames_job", false]], "cacheclear() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.cacheClear", false]], "cachefactory (class in large_image.cache_util)": [[10, "large_image.cache_util.CacheFactory", false]], "cachefactory (class in large_image.cache_util.cachefactory)": [[10, "large_image.cache_util.cachefactory.CacheFactory", false]], "cacheinfo() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.cacheInfo", false]], "cachename (large_image_source_bioformats.bioformatsfiletilesource attribute)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.cacheName", false]], "cachename (large_image_source_bioformats.girder_source.bioformatsgirdertilesource attribute)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource.cacheName", false]], "cachename (large_image_source_deepzoom.deepzoomfiletilesource attribute)": [[17, "large_image_source_deepzoom.DeepzoomFileTileSource.cacheName", false]], "cachename (large_image_source_deepzoom.girder_source.deepzoomgirdertilesource attribute)": [[17, "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource.cacheName", false]], "cachename (large_image_source_dicom.dicomfiletilesource attribute)": [[19, "large_image_source_dicom.DICOMFileTileSource.cacheName", false]], "cachename (large_image_source_dicom.girder_source.dicomgirdertilesource attribute)": [[19, "large_image_source_dicom.girder_source.DICOMGirderTileSource.cacheName", false]], "cachename (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.cacheName", false]], "cachename (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.cacheName", false]], "cachename (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.cacheName", false]], "cachename (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.cacheName", false]], "cachename (large_image_source_multi.girder_source.multigirdertilesource attribute)": [[28, "large_image_source_multi.girder_source.MultiGirderTileSource.cacheName", false]], "cachename (large_image_source_multi.multifiletilesource attribute)": [[28, "large_image_source_multi.MultiFileTileSource.cacheName", false]], "cachename (large_image_source_nd2.girder_source.nd2girdertilesource attribute)": [[30, "large_image_source_nd2.girder_source.ND2GirderTileSource.cacheName", false]], "cachename (large_image_source_nd2.nd2filetilesource attribute)": [[30, "large_image_source_nd2.ND2FileTileSource.cacheName", false]], "cachename (large_image_source_ometiff.girder_source.ometiffgirdertilesource attribute)": [[32, "large_image_source_ometiff.girder_source.OMETiffGirderTileSource.cacheName", false]], "cachename (large_image_source_ometiff.ometifffiletilesource attribute)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.cacheName", false]], "cachename (large_image_source_openjpeg.girder_source.openjpeggirdertilesource attribute)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource.cacheName", false]], "cachename (large_image_source_openjpeg.openjpegfiletilesource attribute)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource.cacheName", false]], "cachename (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.cacheName", false]], "cachename (large_image_source_openslide.openslidefiletilesource attribute)": [[36, "large_image_source_openslide.OpenslideFileTileSource.cacheName", false]], "cachename (large_image_source_pil.girder_source.pilgirdertilesource attribute)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.cacheName", false]], "cachename (large_image_source_pil.pilfiletilesource attribute)": [[38, "large_image_source_pil.PILFileTileSource.cacheName", false]], "cachename (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.cacheName", false]], "cachename (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.cacheName", false]], "cachename (large_image_source_test.testtilesource attribute)": [[42, "large_image_source_test.TestTileSource.cacheName", false]], "cachename (large_image_source_tiff.girder_source.tiffgirdertilesource attribute)": [[44, "large_image_source_tiff.girder_source.TiffGirderTileSource.cacheName", false]], "cachename (large_image_source_tiff.tifffiletilesource attribute)": [[44, "large_image_source_tiff.TiffFileTileSource.cacheName", false]], "cachename (large_image_source_tifffile.girder_source.tifffilegirdertilesource attribute)": [[46, "large_image_source_tifffile.girder_source.TifffileGirderTileSource.cacheName", false]], "cachename (large_image_source_tifffile.tifffilefiletilesource attribute)": [[46, "large_image_source_tifffile.TifffileFileTileSource.cacheName", false]], "cachename (large_image_source_vips.girder_source.vipsgirdertilesource attribute)": [[48, "large_image_source_vips.girder_source.VipsGirderTileSource.cacheName", false]], "cachename (large_image_source_vips.vipsfiletilesource attribute)": [[48, "large_image_source_vips.VipsFileTileSource.cacheName", false]], "cachename (large_image_source_zarr.girder_source.zarrgirdertilesource attribute)": [[50, "large_image_source_zarr.girder_source.ZarrGirderTileSource.cacheName", false]], "cachename (large_image_source_zarr.zarrfiletilesource attribute)": [[50, "large_image_source_zarr.ZarrFileTileSource.cacheName", false]], "cancreatefolderannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.canCreateFolderAnnotations", false]], "canread() (in module large_image.tilesource)": [[11, "large_image.tilesource.canRead", false]], "canread() (in module large_image_source_bioformats)": [[15, "large_image_source_bioformats.canRead", false]], "canread() (in module large_image_source_deepzoom)": [[17, "large_image_source_deepzoom.canRead", false]], "canread() (in module large_image_source_dicom)": [[19, "large_image_source_dicom.canRead", false]], "canread() (in module large_image_source_dummy)": [[22, "large_image_source_dummy.canRead", false]], "canread() (in module large_image_source_gdal)": [[24, "large_image_source_gdal.canRead", false]], "canread() (in module large_image_source_mapnik)": [[26, "large_image_source_mapnik.canRead", false]], "canread() (in module large_image_source_multi)": [[28, "large_image_source_multi.canRead", false]], "canread() (in module large_image_source_nd2)": [[30, "large_image_source_nd2.canRead", false]], "canread() (in module large_image_source_ometiff)": [[32, "large_image_source_ometiff.canRead", false]], "canread() (in module large_image_source_openjpeg)": [[34, "large_image_source_openjpeg.canRead", false]], "canread() (in module large_image_source_openslide)": [[36, "large_image_source_openslide.canRead", false]], "canread() (in module large_image_source_pil)": [[38, "large_image_source_pil.canRead", false]], "canread() (in module large_image_source_rasterio)": [[40, "large_image_source_rasterio.canRead", false]], "canread() (in module large_image_source_test)": [[42, "large_image_source_test.canRead", false]], "canread() (in module large_image_source_tiff)": [[44, "large_image_source_tiff.canRead", false]], "canread() (in module large_image_source_tifffile)": [[46, "large_image_source_tifffile.canRead", false]], "canread() (in module large_image_source_vips)": [[48, "large_image_source_vips.canRead", false]], "canread() (in module large_image_source_zarr)": [[50, "large_image_source_zarr.canRead", false]], "canread() (large_image.tilesource.base.filetilesource class method)": [[11, "large_image.tilesource.base.FileTileSource.canRead", false]], "canread() (large_image.tilesource.base.tilesource class method)": [[11, "large_image.tilesource.base.TileSource.canRead", false]], "canread() (large_image.tilesource.filetilesource class method)": [[11, "large_image.tilesource.FileTileSource.canRead", false]], "canread() (large_image.tilesource.tilesource class method)": [[11, "large_image.tilesource.TileSource.canRead", false]], "canread() (large_image_source_dummy.dummytilesource class method)": [[22, "large_image_source_dummy.DummyTileSource.canRead", false]], "canread() (large_image_source_test.testtilesource class method)": [[42, "large_image_source_test.TestTileSource.canRead", false]], "channelcolors (large_image_source_zarr.zarrfiletilesource property)": [[50, "large_image_source_zarr.ZarrFileTileSource.channelColors", false]], "channelnames (large_image_source_zarr.zarrfiletilesource property)": [[50, "large_image_source_zarr.ZarrFileTileSource.channelNames", false]], "checkforlargeimagefiles() (in module girder_large_image)": [[0, "girder_large_image.checkForLargeImageFiles", false]], "checkformissingdatahandler (class in large_image_source_tifffile)": [[46, "large_image_source_tifffile.checkForMissingDataHandler", false]], "circleshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.circleShapeSchema", false]], "circletype() (girder_large_image_annotation.utils.annotationgeojson method)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON.circleType", false]], "circletype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.circleType", false]], "classcaches (large_image.cache_util.cache.lrucachemetaclass attribute)": [[10, "large_image.cache_util.cache.LruCacheMetaclass.classCaches", false]], "classcaches (large_image.cache_util.lrucachemetaclass attribute)": [[10, "large_image.cache_util.LruCacheMetaclass.classCaches", false]], "clear() (large_image.cache_util.base.basecache method)": [[10, "large_image.cache_util.base.BaseCache.clear", false]], "clear() (large_image.cache_util.memcache method)": [[10, "large_image.cache_util.MemCache.clear", false]], "clear() (large_image.cache_util.memcache.memcache method)": [[10, "large_image.cache_util.memcache.MemCache.clear", false]], "clear() (large_image.cache_util.rediscache method)": [[10, "large_image.cache_util.RedisCache.clear", false]], "clear() (large_image.cache_util.rediscache.rediscache method)": [[10, "large_image.cache_util.rediscache.RedisCache.clear", false]], "client_source_path (girder_large_image.largeimageplugin attribute)": [[0, "girder_large_image.LargeImagePlugin.CLIENT_SOURCE_PATH", false]], "client_source_path (girder_large_image_annotation.largeimageannotationplugin attribute)": [[4, "girder_large_image_annotation.LargeImageAnnotationPlugin.CLIENT_SOURCE_PATH", false]], "client_source_path (large_image_source_dicom.girder_plugin.dicomwebplugin attribute)": [[19, "large_image_source_dicom.girder_plugin.DICOMwebPlugin.CLIENT_SOURCE_PATH", false]], "colorrangeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.colorRangeSchema", false]], "colorschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.colorSchema", false]], "columns (girder_large_image_annotation.utils.plottableitemdata property)": [[7, "girder_large_image_annotation.utils.PlottableItemData.columns", false]], "commoncolumns (girder_large_image_annotation.utils.plottableitemdata attribute)": [[7, "girder_large_image_annotation.utils.PlottableItemData.commonColumns", false]], "configformat() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.configFormat", false]], "configreplace() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.configReplace", false]], "configvalidate() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.configValidate", false]], "convert() (in module large_image_converter)": [[13, "large_image_converter.convert", false]], "convert_image_job() (in module large_image_tasks.tasks)": [[52, "large_image_tasks.tasks.convert_image_job", false]], "convertimage() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.convertImage", false]], "convertimage() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.convertImage", false]], "convertregionscale() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.convertRegionScale", false]], "convertregionscale() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.convertRegionScale", false]], "coordschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.coordSchema", false]], "coordvalueschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.coordValueSchema", false]], "copyannotation() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.copyAnnotation", false]], "corefunctions (large_image_source_tiff.tiff_reader.tiledtiffdirectory attribute)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory.CoreFunctions", false]], "countassociatedimages() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.countAssociatedImages", false]], "countelements() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.countElements", false]], "counthistograms() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.countHistograms", false]], "countthumbnails() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.countThumbnails", false]], "cpu_count() (in module large_image.config)": [[9, "large_image.config.cpu_count", false]], "create_thumbnail_and_label() (in module large_image_converter.format_aperio)": [[13, "large_image_converter.format_aperio.create_thumbnail_and_label", false]], "createannotation() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.createAnnotation", false]], "createannotation() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.createAnnotation", false]], "createimageitem() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.createImageItem", false]], "createitemannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.createItemAnnotations", false]], "createlargeimages() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.createLargeImages", false]], "createthumbnails() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.createThumbnails", false]], "createthumbnailsjob() (in module girder_large_image.rest.large_image_resource)": [[2, "girder_large_image.rest.large_image_resource.createThumbnailsJob", false]], "createthumbnailsjoblog() (in module girder_large_image.rest.large_image_resource)": [[2, "girder_large_image.rest.large_image_resource.createThumbnailsJobLog", false]], "createthumbnailsjobtask() (in module girder_large_image.rest.large_image_resource)": [[2, "girder_large_image.rest.large_image_resource.createThumbnailsJobTask", false]], "createthumbnailsjobthread() (in module girder_large_image.rest.large_image_resource)": [[2, "girder_large_image.rest.large_image_resource.createThumbnailsJobThread", false]], "createtiles() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.createTiles", false]], "crop (large_image_source_vips.vipsfiletilesource property)": [[48, "large_image_source_vips.VipsFileTileSource.crop", false]], "crop (large_image_source_zarr.zarrfiletilesource property)": [[50, "large_image_source_zarr.ZarrFileTileSource.crop", false]], "curritems (large_image.cache_util.base.basecache property)": [[10, "large_image.cache_util.base.BaseCache.curritems", false]], "curritems (large_image.cache_util.memcache property)": [[10, "large_image.cache_util.MemCache.curritems", false]], "curritems (large_image.cache_util.memcache.memcache property)": [[10, "large_image.cache_util.memcache.MemCache.curritems", false]], "curritems (large_image.cache_util.rediscache property)": [[10, "large_image.cache_util.RedisCache.curritems", false]], "curritems (large_image.cache_util.rediscache.rediscache property)": [[10, "large_image.cache_util.rediscache.RedisCache.curritems", false]], "currsize (large_image.cache_util.base.basecache property)": [[10, "large_image.cache_util.base.BaseCache.currsize", false]], "currsize (large_image.cache_util.memcache property)": [[10, "large_image.cache_util.MemCache.currsize", false]], "currsize (large_image.cache_util.memcache.memcache property)": [[10, "large_image.cache_util.memcache.MemCache.currsize", false]], "currsize (large_image.cache_util.rediscache property)": [[10, "large_image.cache_util.RedisCache.currsize", false]], "currsize (large_image.cache_util.rediscache.rediscache property)": [[10, "large_image.cache_util.rediscache.RedisCache.currsize", false]], "cursornextornone() (in module girder_large_image.rest.large_image_resource)": [[2, "girder_large_image.rest.large_image_resource.cursorNextOrNone", false]], "data() (girder_large_image_annotation.utils.plottableitemdata method)": [[7, "girder_large_image_annotation.utils.PlottableItemData.data", false]], "datafileannotationelementselector() (girder_large_image_annotation.utils.plottableitemdata method)": [[7, "girder_large_image_annotation.utils.PlottableItemData.datafileAnnotationElementSelector", false]], "deepzoomfiletilesource (class in large_image_source_deepzoom)": [[17, "large_image_source_deepzoom.DeepzoomFileTileSource", false]], "deepzoomgirdertilesource (class in large_image_source_deepzoom.girder_source)": [[17, "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource", false]], "defaultmaxsize() (large_image_source_pil.girder_source.pilgirdertilesource method)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.defaultMaxSize", false]], "defaultmaxsize() (large_image_source_pil.pilfiletilesource method)": [[38, "large_image_source_pil.PILFileTileSource.defaultMaxSize", false]], "delete() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.delete", false]], "deleteannotation() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.deleteAnnotation", false]], "deleteassociatedimages() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.deleteAssociatedImages", false]], "deletefile() (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.deleteFile", false]], "deletefile() (large_image_source_dicom.assetstore.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.deleteFile", false]], "deletefolderannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.deleteFolderAnnotations", false]], "deletehistograms() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.deleteHistograms", false]], "deleteincompletetiles() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.deleteIncompleteTiles", false]], "deleteitemannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.deleteItemAnnotations", false]], "deletemetadata() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.deleteMetadata", false]], "deletemetadata() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.deleteMetadata", false]], "deletemetadatakey() (girder_large_image.rest.item_meta.internalmetadataitemresource method)": [[2, "girder_large_image.rest.item_meta.InternalMetadataItemResource.deleteMetadataKey", false]], "deleteoldannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.deleteOldAnnotations", false]], "deletethumbnails() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.deleteThumbnails", false]], "deletetiles() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.deleteTiles", false]], "deletetilesthumbnails() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.deleteTilesThumbnails", false]], "dicom_key_to_tag() (in module large_image_source_dicom.dicom_tags)": [[19, "large_image_source_dicom.dicom_tags.dicom_key_to_tag", false]], "dicom_to_dict() (in module large_image_source_dicom)": [[19, "large_image_source_dicom.dicom_to_dict", false]], "dicomfiletilesource (class in large_image_source_dicom)": [[19, "large_image_source_dicom.DICOMFileTileSource", false]], "dicomgirdertilesource (class in large_image_source_dicom.girder_source)": [[19, "large_image_source_dicom.girder_source.DICOMGirderTileSource", false]], "dicomwebassetstoreadapter (class in large_image_source_dicom.assetstore)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter", false]], "dicomwebassetstoreadapter (class in large_image_source_dicom.assetstore.dicomweb_assetstore_adapter)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter", false]], "dicomwebassetstoreresource (class in large_image_source_dicom.assetstore.rest)": [[20, "large_image_source_dicom.assetstore.rest.DICOMwebAssetstoreResource", false]], "dicomwebplugin (class in large_image_source_dicom.girder_plugin)": [[19, "large_image_source_dicom.girder_plugin.DICOMwebPlugin", false]], "dicttoetree() (in module large_image.tilesource)": [[11, "large_image.tilesource.dictToEtree", false]], "dicttoetree() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.dictToEtree", false]], "diffobj() (in module large_image_source_nd2)": [[30, "large_image_source_nd2.diffObj", false]], "display_name (girder_large_image.largeimageplugin attribute)": [[0, "girder_large_image.LargeImagePlugin.DISPLAY_NAME", false]], "display_name (girder_large_image_annotation.largeimageannotationplugin attribute)": [[4, "girder_large_image_annotation.LargeImageAnnotationPlugin.DISPLAY_NAME", false]], "display_name (large_image_source_dicom.girder_plugin.dicomwebplugin attribute)": [[19, "large_image_source_dicom.girder_plugin.DICOMwebPlugin.DISPLAY_NAME", false]], "downloadfile() (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.downloadFile", false]], "downloadfile() (large_image_source_dicom.assetstore.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.downloadFile", false]], "downsampletilehalfres() (in module large_image.tilesource.resample)": [[11, "large_image.tilesource.resample.downsampleTileHalfRes", false]], "dtype (large_image.tilesource.base.tilesource property)": [[11, "large_image.tilesource.base.TileSource.dtype", false]], "dtype (large_image.tilesource.tilesource property)": [[11, "large_image.tilesource.TileSource.dtype", false]], "dummytilesource (class in large_image_source_dummy)": [[22, "large_image_source_dummy.DummyTileSource", false]], "elementcount (girder_large_image_annotation.utils.geojsonannotation property)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.elementCount", false]], "elements (girder_large_image_annotation.utils.geojsonannotation property)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.elements", false]], "elementtogeojson() (girder_large_image_annotation.utils.annotationgeojson method)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON.elementToGeoJSON", false]], "ellipseshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.ellipseShapeSchema", false]], "ellipsetype() (girder_large_image_annotation.utils.annotationgeojson method)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON.ellipseType", false]], "ellipsetype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.ellipseType", false]], "emit() (large_image_source_tifffile.checkformissingdatahandler method)": [[46, "large_image_source_tifffile.checkForMissingDataHandler.emit", false]], "emit() (large_image_tasks.tasks.joblogger method)": [[52, "large_image_tasks.tasks.JobLogger.emit", false]], "et_findall() (in module large_image_source_tifffile)": [[46, "large_image_source_tifffile.et_findall", false]], "etreetodict() (in module large_image.tilesource)": [[11, "large_image.tilesource.etreeToDict", false]], "etreetodict() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.etreeToDict", false]], "existfolderannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.existFolderAnnotations", false]], "expert (girder_large_image_annotation.models.annotation.annotation.skill attribute)": [[5, "girder_large_image_annotation.models.annotation.Annotation.Skill.EXPERT", false]], "extendschema() (in module girder_large_image_annotation.models.annotation)": [[5, "girder_large_image_annotation.models.annotation.extendSchema", false]], "extensions (large_image.tilesource.base.tilesource attribute)": [[11, "large_image.tilesource.base.TileSource.extensions", false]], "extensions (large_image.tilesource.geo.gdalbasefiletilesource attribute)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.extensions", false]], "extensions (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.extensions", false]], "extensions (large_image_source_bioformats.bioformatsfiletilesource attribute)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.extensions", false]], "extensions (large_image_source_deepzoom.deepzoomfiletilesource attribute)": [[17, "large_image_source_deepzoom.DeepzoomFileTileSource.extensions", false]], "extensions (large_image_source_dicom.dicomfiletilesource attribute)": [[19, "large_image_source_dicom.DICOMFileTileSource.extensions", false]], "extensions (large_image_source_dummy.dummytilesource attribute)": [[22, "large_image_source_dummy.DummyTileSource.extensions", false]], "extensions (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.extensions", false]], "extensions (large_image_source_multi.multifiletilesource attribute)": [[28, "large_image_source_multi.MultiFileTileSource.extensions", false]], "extensions (large_image_source_nd2.nd2filetilesource attribute)": [[30, "large_image_source_nd2.ND2FileTileSource.extensions", false]], "extensions (large_image_source_ometiff.ometifffiletilesource attribute)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.extensions", false]], "extensions (large_image_source_openjpeg.openjpegfiletilesource attribute)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource.extensions", false]], "extensions (large_image_source_openslide.openslidefiletilesource attribute)": [[36, "large_image_source_openslide.OpenslideFileTileSource.extensions", false]], "extensions (large_image_source_pil.pilfiletilesource attribute)": [[38, "large_image_source_pil.PILFileTileSource.extensions", false]], "extensions (large_image_source_test.testtilesource attribute)": [[42, "large_image_source_test.TestTileSource.extensions", false]], "extensions (large_image_source_tiff.tifffiletilesource attribute)": [[44, "large_image_source_tiff.TiffFileTileSource.extensions", false]], "extensions (large_image_source_tifffile.tifffilefiletilesource attribute)": [[46, "large_image_source_tifffile.TifffileFileTileSource.extensions", false]], "extensions (large_image_source_vips.vipsfiletilesource attribute)": [[48, "large_image_source_vips.VipsFileTileSource.extensions", false]], "extensions (large_image_source_zarr.zarrfiletilesource attribute)": [[50, "large_image_source_zarr.ZarrFileTileSource.extensions", false]], "extensionswithadjacentfiles (girder_large_image.girder_tilesource.girdertilesource attribute)": [[0, "girder_large_image.girder_tilesource.GirderTileSource.extensionsWithAdjacentFiles", false]], "extensionswithadjacentfiles (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.extensionsWithAdjacentFiles", false]], "extract_dicom_metadata() (in module large_image_source_dicom.dicom_metadata)": [[19, "large_image_source_dicom.dicom_metadata.extract_dicom_metadata", false]], "extract_specimen_metadata() (in module large_image_source_dicom.dicom_metadata)": [[19, "large_image_source_dicom.dicom_metadata.extract_specimen_metadata", false]], "fallback (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.FALLBACK", false]], "fallback_high (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.FALLBACK_HIGH", false]], "filetilesource (class in large_image.tilesource)": [[11, "large_image.tilesource.FileTileSource", false]], "filetilesource (class in large_image.tilesource.base)": [[11, "large_image.tilesource.base.FileTileSource", false]], "finalizeupload() (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.finalizeUpload", false]], "finalizeupload() (large_image_source_dicom.assetstore.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.finalizeUpload", false]], "find() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.find", false]], "findannotatedimages() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.findAnnotatedImages", false]], "findannotatedimages() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.findAnnotatedImages", false]], "format_hook() (in module large_image_converter)": [[13, "large_image_converter.format_hook", false]], "fractaltile() (large_image_source_test.testtilesource method)": [[42, "large_image_source_test.TestTileSource.fractalTile", false]], "frames (large_image.tilesource.base.tilesource property)": [[11, "large_image.tilesource.base.TileSource.frames", false]], "frames (large_image.tilesource.tilesource property)": [[11, "large_image.tilesource.TileSource.frames", false]], "from_map() (large_image.tilesource.jupyter.map method)": [[11, "large_image.tilesource.jupyter.Map.from_map", false]], "fullalphavalue() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.fullAlphaValue", false]], "gdalbasefiletilesource (class in large_image.tilesource.geo)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource", false]], "gdalfiletilesource (class in large_image_source_gdal)": [[24, "large_image_source_gdal.GDALFileTileSource", false]], "gdalgirdertilesource (class in large_image_source_gdal.girder_source)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource", false]], "geobasefiletilesource (class in large_image.tilesource.geo)": [[11, "large_image.tilesource.geo.GeoBaseFileTileSource", false]], "geojson (girder_large_image_annotation.utils.annotationgeojson property)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON.geojson", false]], "geojson() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.geojson", false]], "geojsonannotation (class in girder_large_image_annotation.utils)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation", false]], "geospatial (large_image.tilesource.base.tilesource property)": [[11, "large_image.tilesource.base.TileSource.geospatial", false]], "geospatial (large_image.tilesource.geo.gdalbasefiletilesource property)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.geospatial", false]], "geospatial (large_image.tilesource.tilesource property)": [[11, "large_image.tilesource.TileSource.geospatial", false]], "geospatial (large_image_source_gdal.gdalfiletilesource property)": [[24, "large_image_source_gdal.GDALFileTileSource.geospatial", false]], "get_dicomweb_metadata() (in module large_image_source_dicom.dicomweb_utils)": [[19, "large_image_source_dicom.dicomweb_utils.get_dicomweb_metadata", false]], "get_first_wsi_volume_metadata() (in module large_image_source_dicom.dicomweb_utils)": [[19, "large_image_source_dicom.dicomweb_utils.get_first_wsi_volume_metadata", false]], "getandcacheimageordatarun() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getAndCacheImageOrDataRun", false]], "getannotation() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getAnnotation", false]], "getannotationaccess() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getAnnotationAccess", false]], "getannotationhistory() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getAnnotationHistory", false]], "getannotationhistorylist() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getAnnotationHistoryList", false]], "getannotationschema() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getAnnotationSchema", false]], "getannotationwithformat() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getAnnotationWithFormat", false]], "getassociatedimage() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getAssociatedImage", false]], "getassociatedimage() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getAssociatedImage", false]], "getassociatedimage() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getAssociatedImage", false]], "getassociatedimage() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getAssociatedImage", false]], "getassociatedimage() (large_image_source_multi.multifiletilesource method)": [[28, "large_image_source_multi.MultiFileTileSource.getAssociatedImage", false]], "getassociatedimagemetadata() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getAssociatedImageMetadata", false]], "getassociatedimageslist() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getAssociatedImagesList", false]], "getassociatedimageslist() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image_source_bioformats.bioformatsfiletilesource method)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image_source_dicom.dicomfiletilesource method)": [[19, "large_image_source_dicom.DICOMFileTileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image_source_multi.multifiletilesource method)": [[28, "large_image_source_multi.MultiFileTileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image_source_openjpeg.openjpegfiletilesource method)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image_source_openslide.openslidefiletilesource method)": [[36, "large_image_source_openslide.OpenslideFileTileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image_source_tiff.tifffiletilesource method)": [[44, "large_image_source_tiff.TiffFileTileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image_source_tifffile.tifffilefiletilesource method)": [[46, "large_image_source_tifffile.TifffileFileTileSource.getAssociatedImagesList", false]], "getassociatedimageslist() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.getAssociatedImagesList", false]], "getavailablenamedpalettes() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.getAvailableNamedPalettes", false]], "getbandinformation() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getBandInformation", false]], "getbandinformation() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getBandInformation", false]], "getbandinformation() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getBandInformation", false]], "getbandinformation() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getBandInformation", false]], "getbandinformation() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getBandInformation", false]], "getbandinformation() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getBandInformation", false]], "getbounds() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getBounds", false]], "getbounds() (large_image.tilesource.geo.gdalbasefiletilesource method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.getBounds", false]], "getbounds() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getBounds", false]], "getbounds() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getBounds", false]], "getbounds() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getBounds", false]], "getcache() (large_image.cache_util.base.basecache static method)": [[10, "large_image.cache_util.base.BaseCache.getCache", false]], "getcache() (large_image.cache_util.cachefactory method)": [[10, "large_image.cache_util.CacheFactory.getCache", false]], "getcache() (large_image.cache_util.cachefactory.cachefactory method)": [[10, "large_image.cache_util.cachefactory.CacheFactory.getCache", false]], "getcache() (large_image.cache_util.memcache static method)": [[10, "large_image.cache_util.MemCache.getCache", false]], "getcache() (large_image.cache_util.memcache.memcache static method)": [[10, "large_image.cache_util.memcache.MemCache.getCache", false]], "getcache() (large_image.cache_util.rediscache static method)": [[10, "large_image.cache_util.RedisCache.getCache", false]], "getcache() (large_image.cache_util.rediscache.rediscache static method)": [[10, "large_image.cache_util.rediscache.RedisCache.getCache", false]], "getcachesize() (large_image.cache_util.cachefactory method)": [[10, "large_image.cache_util.CacheFactory.getCacheSize", false]], "getcachesize() (large_image.cache_util.cachefactory.cachefactory method)": [[10, "large_image.cache_util.cachefactory.CacheFactory.getCacheSize", false]], "getcenter() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getCenter", false]], "getcenter() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getCenter", false]], "getconfig() (in module large_image.config)": [[9, "large_image.config.getConfig", false]], "getcrs() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getCrs", false]], "getdziinfo() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getDZIInfo", false]], "getdzitile() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getDZITile", false]], "getelementgroupset() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.getElementGroupSet", false]], "getelements() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.getElements", false]], "getfilesize() (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.getFileSize", false]], "getfilesize() (large_image_source_dicom.assetstore.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.getFileSize", false]], "getfirstavailablecache() (in module large_image.cache_util.cachefactory)": [[10, "large_image.cache_util.cachefactory.getFirstAvailableCache", false]], "getfolderannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getFolderAnnotations", false]], "getgirdertilesource() (in module girder_large_image.girder_tilesource)": [[0, "girder_large_image.girder_tilesource.getGirderTileSource", false]], "getgirdertilesourcename() (in module girder_large_image.girder_tilesource)": [[0, "girder_large_image.girder_tilesource.getGirderTileSourceName", false]], "gethexcolors() (large_image.tilesource.geo.gdalbasefiletilesource static method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.getHexColors", false]], "gethistogram() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getHistogram", false]], "geticcprofiles() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getICCProfiles", false]], "geticcprofiles() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getICCProfiles", false]], "getinternalmetadata() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getInternalMetadata", false]], "getinternalmetadata() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getInternalMetadata", false]], "getinternalmetadata() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_bioformats.bioformatsfiletilesource method)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_deepzoom.deepzoomfiletilesource method)": [[17, "large_image_source_deepzoom.DeepzoomFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_dicom.dicomfiletilesource method)": [[19, "large_image_source_dicom.DICOMFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_multi.multifiletilesource method)": [[28, "large_image_source_multi.MultiFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_nd2.nd2filetilesource method)": [[30, "large_image_source_nd2.ND2FileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_ometiff.ometifffiletilesource method)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_openjpeg.openjpegfiletilesource method)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_openslide.openslidefiletilesource method)": [[36, "large_image_source_openslide.OpenslideFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_pil.pilfiletilesource method)": [[38, "large_image_source_pil.PILFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_test.testtilesource method)": [[42, "large_image_source_test.TestTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_tiff.tifffiletilesource method)": [[44, "large_image_source_tiff.TiffFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_tifffile.tifffilefiletilesource method)": [[46, "large_image_source_tifffile.TifffileFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_vips.vipsfiletilesource method)": [[48, "large_image_source_vips.VipsFileTileSource.getInternalMetadata", false]], "getinternalmetadata() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.getInternalMetadata", false]], "getitemannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getItemAnnotations", false]], "getitemlistannotationcounts() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getItemListAnnotationCounts", false]], "getitemplottabledata() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getItemPlottableData", false]], "getitemplottableelements() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getItemPlottableElements", false]], "getlevelformagnification() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getLevelForMagnification", false]], "getlevelformagnification() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getLevelForMagnification", false]], "getlogger() (in module large_image.config)": [[9, "large_image.config.getLogger", false]], "getlruhash() (girder_large_image.girder_tilesource.girdertilesource static method)": [[0, "girder_large_image.girder_tilesource.GirderTileSource.getLRUHash", false]], "getlruhash() (large_image.tilesource.base.filetilesource static method)": [[11, "large_image.tilesource.base.FileTileSource.getLRUHash", false]], "getlruhash() (large_image.tilesource.base.tilesource static method)": [[11, "large_image.tilesource.base.TileSource.getLRUHash", false]], "getlruhash() (large_image.tilesource.filetilesource static method)": [[11, "large_image.tilesource.FileTileSource.getLRUHash", false]], "getlruhash() (large_image.tilesource.tilesource static method)": [[11, "large_image.tilesource.TileSource.getLRUHash", false]], "getlruhash() (large_image_source_gdal.gdalfiletilesource static method)": [[24, "large_image_source_gdal.GDALFileTileSource.getLRUHash", false]], "getlruhash() (large_image_source_gdal.girder_source.gdalgirdertilesource static method)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.getLRUHash", false]], "getlruhash() (large_image_source_pil.girder_source.pilgirdertilesource static method)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.getLRUHash", false]], "getlruhash() (large_image_source_pil.pilfiletilesource static method)": [[38, "large_image_source_pil.PILFileTileSource.getLRUHash", false]], "getlruhash() (large_image_source_rasterio.girder_source.rasteriogirdertilesource static method)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.getLRUHash", false]], "getlruhash() (large_image_source_rasterio.rasteriofiletilesource static method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getLRUHash", false]], "getlruhash() (large_image_source_test.testtilesource static method)": [[42, "large_image_source_test.TestTileSource.getLRUHash", false]], "getmagnificationforlevel() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getMagnificationForLevel", false]], "getmagnificationforlevel() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getMagnificationForLevel", false]], "getmaxsize() (in module large_image_source_pil)": [[38, "large_image_source_pil.getMaxSize", false]], "getmetadata() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getMetadata", false]], "getmetadata() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getMetadata", false]], "getmetadata() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getMetadata", false]], "getmetadata() (large_image_source_bioformats.bioformatsfiletilesource method)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_dicom.dicomfiletilesource method)": [[19, "large_image_source_dicom.DICOMFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_multi.multifiletilesource method)": [[28, "large_image_source_multi.MultiFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_nd2.nd2filetilesource method)": [[30, "large_image_source_nd2.ND2FileTileSource.getMetadata", false]], "getmetadata() (large_image_source_ometiff.ometifffiletilesource method)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_pil.pilfiletilesource method)": [[38, "large_image_source_pil.PILFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_test.testtilesource method)": [[42, "large_image_source_test.TestTileSource.getMetadata", false]], "getmetadata() (large_image_source_tiff.tifffiletilesource method)": [[44, "large_image_source_tiff.TiffFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_tifffile.tifffilefiletilesource method)": [[46, "large_image_source_tifffile.TifffileFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_vips.vipsfiletilesource method)": [[48, "large_image_source_vips.VipsFileTileSource.getMetadata", false]], "getmetadata() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.getMetadata", false]], "getmetadatakey() (girder_large_image.rest.item_meta.internalmetadataitemresource method)": [[2, "girder_large_image.rest.item_meta.InternalMetadataItemResource.getMetadataKey", false]], "getnativemagnification() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getNativeMagnification", false]], "getnativemagnification() (large_image.tilesource.geo.gdalbasefiletilesource method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_bioformats.bioformatsfiletilesource method)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_dicom.dicomfiletilesource method)": [[19, "large_image_source_dicom.DICOMFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_multi.multifiletilesource method)": [[28, "large_image_source_multi.MultiFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_nd2.nd2filetilesource method)": [[30, "large_image_source_nd2.ND2FileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_ometiff.ometifffiletilesource method)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_openjpeg.openjpegfiletilesource method)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_openslide.openslidefiletilesource method)": [[36, "large_image_source_openslide.OpenslideFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_tiff.tifffiletilesource method)": [[44, "large_image_source_tiff.TiffFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_tifffile.tifffilefiletilesource method)": [[46, "large_image_source_tifffile.TifffileFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_vips.vipsfiletilesource method)": [[48, "large_image_source_vips.VipsFileTileSource.getNativeMagnification", false]], "getnativemagnification() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.getNativeMagnification", false]], "getnextversionvalue() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.getNextVersionValue", false]], "getoldannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.getOldAnnotations", false]], "getonebandinformation() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getOneBandInformation", false]], "getonebandinformation() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getOneBandInformation", false]], "getonebandinformation() (large_image_source_mapnik.mapnikfiletilesource method)": [[26, "large_image_source_mapnik.MapnikFileTileSource.getOneBandInformation", false]], "getpalettecolors() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.getPaletteColors", false]], "getpixel() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getPixel", false]], "getpixel() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getPixel", false]], "getpixel() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getPixel", false]], "getpixel() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getPixel", false]], "getpixel() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getPixel", false]], "getpixelsizeinmeters() (large_image.tilesource.geo.gdalbasefiletilesource method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.getPixelSizeInMeters", false]], "getpointatanotherscale() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getPointAtAnotherScale", false]], "getpointatanotherscale() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getPointAtAnotherScale", false]], "getpreferredlevel() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getPreferredLevel", false]], "getpreferredlevel() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getPreferredLevel", false]], "getpreferredlevel() (large_image_source_ometiff.ometifffiletilesource method)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.getPreferredLevel", false]], "getpreferredlevel() (large_image_source_openslide.openslidefiletilesource method)": [[36, "large_image_source_openslide.OpenslideFileTileSource.getPreferredLevel", false]], "getproj4string() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getProj4String", false]], "getpublicsettings() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.getPublicSettings", false]], "getregion() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getRegion", false]], "getregion() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getRegion", false]], "getregion() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getRegion", false]], "getregion() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getRegion", false]], "getregion() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getRegion", false]], "getregionatanotherscale() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getRegionAtAnotherScale", false]], "getregionatanotherscale() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getRegionAtAnotherScale", false]], "getsingletile() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getSingleTile", false]], "getsingletile() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getSingleTile", false]], "getsingletileatanotherscale() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getSingleTileAtAnotherScale", false]], "getsingletileatanotherscale() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getSingleTileAtAnotherScale", false]], "getsourcenamefromdict() (in module large_image.tilesource)": [[11, "large_image.tilesource.getSourceNameFromDict", false]], "getstate() (girder_large_image.girder_tilesource.girdertilesource method)": [[0, "girder_large_image.girder_tilesource.GirderTileSource.getState", false]], "getstate() (large_image.tilesource.base.filetilesource method)": [[11, "large_image.tilesource.base.FileTileSource.getState", false]], "getstate() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getState", false]], "getstate() (large_image.tilesource.filetilesource method)": [[11, "large_image.tilesource.FileTileSource.getState", false]], "getstate() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getState", false]], "getstate() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getState", false]], "getstate() (large_image_source_pil.girder_source.pilgirdertilesource method)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.getState", false]], "getstate() (large_image_source_pil.pilfiletilesource method)": [[38, "large_image_source_pil.PILFileTileSource.getState", false]], "getstate() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getState", false]], "getstate() (large_image_source_test.testtilesource method)": [[42, "large_image_source_test.TestTileSource.getState", false]], "getstate() (large_image_source_vips.vipsfiletilesource method)": [[48, "large_image_source_vips.VipsFileTileSource.getState", false]], "getstate() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.getState", false]], "gettesttile() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getTestTile", false]], "gettesttilesinfo() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getTestTilesInfo", false]], "getthumbnail() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getThumbnail", false]], "getthumbnail() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getThumbnail", false]], "getthumbnail() (large_image.tilesource.geo.gdalbasefiletilesource method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.getThumbnail", false]], "getthumbnail() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getThumbnail", false]], "gettiffdir() (large_image_source_tiff.tifffiletilesource method)": [[44, "large_image_source_tiff.TiffFileTileSource.getTiffDir", false]], "gettile() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.getTile", false]], "gettile() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getTile", false]], "gettile() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getTile", false]], "gettile() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getTile", false]], "gettile() (large_image_source_bioformats.bioformatsfiletilesource method)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.getTile", false]], "gettile() (large_image_source_deepzoom.deepzoomfiletilesource method)": [[17, "large_image_source_deepzoom.DeepzoomFileTileSource.getTile", false]], "gettile() (large_image_source_dicom.dicomfiletilesource method)": [[19, "large_image_source_dicom.DICOMFileTileSource.getTile", false]], "gettile() (large_image_source_dummy.dummytilesource method)": [[22, "large_image_source_dummy.DummyTileSource.getTile", false]], "gettile() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.getTile", false]], "gettile() (large_image_source_mapnik.mapnikfiletilesource method)": [[26, "large_image_source_mapnik.MapnikFileTileSource.getTile", false]], "gettile() (large_image_source_multi.multifiletilesource method)": [[28, "large_image_source_multi.MultiFileTileSource.getTile", false]], "gettile() (large_image_source_nd2.nd2filetilesource method)": [[30, "large_image_source_nd2.ND2FileTileSource.getTile", false]], "gettile() (large_image_source_ometiff.ometifffiletilesource method)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.getTile", false]], "gettile() (large_image_source_openjpeg.openjpegfiletilesource method)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource.getTile", false]], "gettile() (large_image_source_openslide.openslidefiletilesource method)": [[36, "large_image_source_openslide.OpenslideFileTileSource.getTile", false]], "gettile() (large_image_source_pil.girder_source.pilgirdertilesource method)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.getTile", false]], "gettile() (large_image_source_pil.pilfiletilesource method)": [[38, "large_image_source_pil.PILFileTileSource.getTile", false]], "gettile() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.getTile", false]], "gettile() (large_image_source_test.testtilesource method)": [[42, "large_image_source_test.TestTileSource.getTile", false]], "gettile() (large_image_source_tiff.tiff_reader.tiledtiffdirectory method)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory.getTile", false]], "gettile() (large_image_source_tiff.tifffiletilesource method)": [[44, "large_image_source_tiff.TiffFileTileSource.getTile", false]], "gettile() (large_image_source_tifffile.tifffilefiletilesource method)": [[46, "large_image_source_tifffile.TifffileFileTileSource.getTile", false]], "gettile() (large_image_source_vips.vipsfiletilesource method)": [[48, "large_image_source_vips.VipsFileTileSource.getTile", false]], "gettile() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.getTile", false]], "gettilecache() (in module large_image.cache_util)": [[10, "large_image.cache_util.getTileCache", false]], "gettilecache() (in module large_image.cache_util.cache)": [[10, "large_image.cache_util.cache.getTileCache", false]], "gettilecorners() (large_image.tilesource.geo.gdalbasefiletilesource method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.getTileCorners", false]], "gettilecount() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getTileCount", false]], "gettilecount() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getTileCount", false]], "gettileframesquadinfo() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.getTileFramesQuadInfo", false]], "gettileiotifferror() (large_image_source_tiff.tifffiletilesource method)": [[44, "large_image_source_tiff.TiffFileTileSource.getTileIOTiffError", false]], "gettilemimetype() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.getTileMimeType", false]], "gettilemimetype() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.getTileMimeType", false]], "gettilesinfo() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getTilesInfo", false]], "gettilesource() (in module large_image.tilesource)": [[11, "large_image.tilesource.getTileSource", false]], "gettilespixel() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getTilesPixel", false]], "gettilesregion() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getTilesRegion", false]], "gettilesthumbnail() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getTilesThumbnail", false]], "gettilewithframe() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.getTileWithFrame", false]], "getversion() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.getVersion", false]], "getyamlconfigfile() (in module girder_large_image.rest)": [[2, "girder_large_image.rest.getYAMLConfigFile", false]], "girder_large_image": [[0, "module-girder_large_image", false]], "girder_large_image.constants": [[0, "module-girder_large_image.constants", false]], "girder_large_image.girder_tilesource": [[0, "module-girder_large_image.girder_tilesource", false]], "girder_large_image.loadmodelcache": [[0, "module-girder_large_image.loadmodelcache", false]], "girder_large_image.models": [[1, "module-girder_large_image.models", false]], "girder_large_image.models.image_item": [[1, "module-girder_large_image.models.image_item", false]], "girder_large_image.rest": [[2, "module-girder_large_image.rest", false]], "girder_large_image.rest.item_meta": [[2, "module-girder_large_image.rest.item_meta", false]], "girder_large_image.rest.large_image_resource": [[2, "module-girder_large_image.rest.large_image_resource", false]], "girder_large_image.rest.tiles": [[2, "module-girder_large_image.rest.tiles", false]], "girder_large_image_annotation": [[4, "module-girder_large_image_annotation", false]], "girder_large_image_annotation.constants": [[4, "module-girder_large_image_annotation.constants", false]], "girder_large_image_annotation.handlers": [[4, "module-girder_large_image_annotation.handlers", false]], "girder_large_image_annotation.models": [[5, "module-girder_large_image_annotation.models", false]], "girder_large_image_annotation.models.annotation": [[5, "module-girder_large_image_annotation.models.annotation", false]], "girder_large_image_annotation.models.annotationelement": [[5, "module-girder_large_image_annotation.models.annotationelement", false]], "girder_large_image_annotation.rest": [[6, "module-girder_large_image_annotation.rest", false]], "girder_large_image_annotation.rest.annotation": [[6, "module-girder_large_image_annotation.rest.annotation", false]], "girder_large_image_annotation.utils": [[7, "module-girder_large_image_annotation.utils", false]], "girdersource (girder_large_image.girder_tilesource.girdertilesource attribute)": [[0, "girder_large_image.girder_tilesource.GirderTileSource.girderSource", false]], "girdertilesource (class in girder_large_image.girder_tilesource)": [[0, "girder_large_image.girder_tilesource.GirderTileSource", false]], "griddataschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.griddataSchema", false]], "groupschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.groupSchema", false]], "handlecopyitem() (in module girder_large_image)": [[0, "girder_large_image.handleCopyItem", false]], "handlefilesave() (in module girder_large_image)": [[0, "girder_large_image.handleFileSave", false]], "handleremovefile() (in module girder_large_image)": [[0, "girder_large_image.handleRemoveFile", false]], "handlesettingsave() (in module girder_large_image)": [[0, "girder_large_image.handleSettingSave", false]], "heatmapschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.heatmapSchema", false]], "high (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.HIGH", false]], "higher (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.HIGHER", false]], "histogram() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.histogram", false]], "histogram() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.histogram", false]], "histogram() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.histogram", false]], "histogramthreshold() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.histogramThreshold", false]], "id (large_image.tilesource.jupyter.map property)": [[11, "large_image.tilesource.jupyter.Map.id", false]], "idregex (girder_large_image_annotation.models.annotation.annotation attribute)": [[5, "girder_large_image_annotation.models.annotation.Annotation.idRegex", false]], "imagebytes (class in large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.ImageBytes", false]], "imagedescription (large_image_source_zarr.zarrfiletilesource property)": [[50, "large_image_source_zarr.ZarrFileTileSource.imageDescription", false]], "imageheight (large_image_source_tiff.tiff_reader.tiledtiffdirectory property)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory.imageHeight", false]], "imageitem (class in girder_large_image.models.image_item)": [[1, "girder_large_image.models.image_item.ImageItem", false]], "imagewidth (large_image_source_tiff.tiff_reader.tiledtiffdirectory property)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory.imageWidth", false]], "implicit (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.IMPLICIT", false]], "implicit_high (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.IMPLICIT_HIGH", false]], "importdata() (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.importData", false]], "importdata() (large_image_source_dicom.assetstore.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.importData", false]], "importdata() (large_image_source_dicom.assetstore.rest.dicomwebassetstoreresource method)": [[20, "large_image_source_dicom.assetstore.rest.DICOMwebAssetstoreResource.importData", false]], "initialize() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.initialize", false]], "initialize() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.initialize", false]], "initialize() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.initialize", false]], "initupload() (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.initUpload", false]], "initupload() (large_image_source_dicom.assetstore.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.initUpload", false]], "injectannotationgroupset() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.injectAnnotationGroupSet", false]], "internalmetadataitemresource (class in girder_large_image.rest.item_meta)": [[2, "girder_large_image.rest.item_meta.InternalMetadataItemResource", false]], "interpolateminmax() (large_image_source_mapnik.mapnikfiletilesource static method)": [[26, "large_image_source_mapnik.MapnikFileTileSource.interpolateMinMax", false]], "invalidateloadmodelcache() (in module girder_large_image.loadmodelcache)": [[0, "girder_large_image.loadmodelcache.invalidateLoadModelCache", false]], "invalidoperationtifferror": [[44, "large_image_source_tiff.exceptions.InvalidOperationTiffError", false]], "ioopentifferror": [[44, "large_image_source_tiff.exceptions.IOOpenTiffError", false]], "iotifferror": [[44, "large_image_source_tiff.exceptions.IOTiffError", false]], "iplmap (large_image.tilesource.jupyter.ipyleafletmixin property)": [[11, "large_image.tilesource.jupyter.IPyLeafletMixin.iplmap", false]], "ipyleafletmixin (class in large_image.tilesource.jupyter)": [[11, "large_image.tilesource.jupyter.IPyLeafletMixin", false]], "is_geospatial() (in module large_image_converter)": [[13, "large_image_converter.is_geospatial", false]], "is_vips() (in module large_image_converter)": [[13, "large_image_converter.is_vips", false]], "isgeojson() (in module girder_large_image_annotation.utils)": [[7, "girder_large_image_annotation.utils.isGeoJSON", false]], "isgeospatial() (large_image.tilesource.geo.gdalbasefiletilesource static method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.isGeospatial", false]], "isgeospatial() (large_image_source_gdal.gdalfiletilesource static method)": [[24, "large_image_source_gdal.GDALFileTileSource.isGeospatial", false]], "isgeospatial() (large_image_source_rasterio.rasteriofiletilesource static method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.isGeospatial", false]], "istilecachesetup() (in module large_image.cache_util)": [[10, "large_image.cache_util.isTileCacheSetup", false]], "istilecachesetup() (in module large_image.cache_util.cache)": [[10, "large_image.cache_util.cache.isTileCacheSetup", false]], "isvalidpalette() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.isValidPalette", false]], "itemnameidselector() (girder_large_image_annotation.utils.plottableitemdata method)": [[7, "girder_large_image_annotation.utils.PlottableItemData.itemNameIDSelector", false]], "joblogger (class in large_image_tasks.tasks)": [[52, "large_image_tasks.tasks.JobLogger", false]], "json_serial() (in module large_image_converter)": [[13, "large_image_converter.json_serial", false]], "jsondict (class in large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.JSONDict", false]], "jupyter_host (large_image.tilesource.jupyter.ipyleafletmixin attribute)": [[11, "large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_HOST", false]], "jupyter_proxy (large_image.tilesource.jupyter.ipyleafletmixin attribute)": [[11, "large_image.tilesource.jupyter.IPyLeafletMixin.JUPYTER_PROXY", false]], "keyselector() (girder_large_image_annotation.utils.plottableitemdata static method)": [[7, "girder_large_image_annotation.utils.PlottableItemData.keySelector", false]], "labelschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.labelSchema", false]], "large_image": [[9, "module-large_image", false]], "large_image.cache_util": [[10, "module-large_image.cache_util", false]], "large_image.cache_util.base": [[10, "module-large_image.cache_util.base", false]], "large_image.cache_util.cache": [[10, "module-large_image.cache_util.cache", false]], "large_image.cache_util.cachefactory": [[10, "module-large_image.cache_util.cachefactory", false]], "large_image.cache_util.memcache": [[10, "module-large_image.cache_util.memcache", false]], "large_image.cache_util.rediscache": [[10, "module-large_image.cache_util.rediscache", false]], "large_image.config": [[9, "module-large_image.config", false]], "large_image.constants": [[9, "module-large_image.constants", false]], "large_image.exceptions": [[9, "module-large_image.exceptions", false]], "large_image.tilesource": [[11, "module-large_image.tilesource", false]], "large_image.tilesource.base": [[11, "module-large_image.tilesource.base", false]], "large_image.tilesource.geo": [[11, "module-large_image.tilesource.geo", false]], "large_image.tilesource.jupyter": [[11, "module-large_image.tilesource.jupyter", false]], "large_image.tilesource.resample": [[11, "module-large_image.tilesource.resample", false]], "large_image.tilesource.stylefuncs": [[11, "module-large_image.tilesource.stylefuncs", false]], "large_image.tilesource.tiledict": [[11, "module-large_image.tilesource.tiledict", false]], "large_image.tilesource.tileiterator": [[11, "module-large_image.tilesource.tileiterator", false]], "large_image.tilesource.utilities": [[11, "module-large_image.tilesource.utilities", false]], "large_image_auto_set (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_AUTO_SET", false]], "large_image_auto_use_all_files (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_AUTO_USE_ALL_FILES", false]], "large_image_config_folder (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_CONFIG_FOLDER", false]], "large_image_converter": [[13, "module-large_image_converter", false]], "large_image_converter.format_aperio": [[13, "module-large_image_converter.format_aperio", false]], "large_image_default_viewer (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_DEFAULT_VIEWER", false]], "large_image_icc_correction (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_ICC_CORRECTION", false]], "large_image_max_small_image_size (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE", false]], "large_image_max_thumbnail_files (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_MAX_THUMBNAIL_FILES", false]], "large_image_notification_stream_fallback (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK", false]], "large_image_show_extra (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_SHOW_EXTRA", false]], "large_image_show_extra_admin (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_SHOW_EXTRA_ADMIN", false]], "large_image_show_extra_public (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_SHOW_EXTRA_PUBLIC", false]], "large_image_show_item_extra (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_SHOW_ITEM_EXTRA", false]], "large_image_show_item_extra_admin (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_SHOW_ITEM_EXTRA_ADMIN", false]], "large_image_show_item_extra_public (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_SHOW_ITEM_EXTRA_PUBLIC", false]], "large_image_show_thumbnails (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_SHOW_THUMBNAILS", false]], "large_image_show_viewer (girder_large_image.constants.pluginsettings attribute)": [[0, "girder_large_image.constants.PluginSettings.LARGE_IMAGE_SHOW_VIEWER", false]], "large_image_source_bioformats": [[15, "module-large_image_source_bioformats", false]], "large_image_source_bioformats.girder_source": [[15, "module-large_image_source_bioformats.girder_source", false]], "large_image_source_deepzoom": [[17, "module-large_image_source_deepzoom", false]], "large_image_source_deepzoom.girder_source": [[17, "module-large_image_source_deepzoom.girder_source", false]], "large_image_source_dicom": [[19, "module-large_image_source_dicom", false]], "large_image_source_dicom.assetstore": [[20, "module-large_image_source_dicom.assetstore", false]], "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter": [[20, "module-large_image_source_dicom.assetstore.dicomweb_assetstore_adapter", false]], "large_image_source_dicom.assetstore.rest": [[20, "module-large_image_source_dicom.assetstore.rest", false]], "large_image_source_dicom.dicom_metadata": [[19, "module-large_image_source_dicom.dicom_metadata", false]], "large_image_source_dicom.dicom_tags": [[19, "module-large_image_source_dicom.dicom_tags", false]], "large_image_source_dicom.dicomweb_utils": [[19, "module-large_image_source_dicom.dicomweb_utils", false]], "large_image_source_dicom.girder_plugin": [[19, "module-large_image_source_dicom.girder_plugin", false]], "large_image_source_dicom.girder_source": [[19, "module-large_image_source_dicom.girder_source", false]], "large_image_source_dummy": [[22, "module-large_image_source_dummy", false]], "large_image_source_gdal": [[24, "module-large_image_source_gdal", false]], "large_image_source_gdal.girder_source": [[24, "module-large_image_source_gdal.girder_source", false]], "large_image_source_mapnik": [[26, "module-large_image_source_mapnik", false]], "large_image_source_mapnik.girder_source": [[26, "module-large_image_source_mapnik.girder_source", false]], "large_image_source_multi": [[28, "module-large_image_source_multi", false]], "large_image_source_multi.girder_source": [[28, "module-large_image_source_multi.girder_source", false]], "large_image_source_nd2": [[30, "module-large_image_source_nd2", false]], "large_image_source_nd2.girder_source": [[30, "module-large_image_source_nd2.girder_source", false]], "large_image_source_ometiff": [[32, "module-large_image_source_ometiff", false]], "large_image_source_ometiff.girder_source": [[32, "module-large_image_source_ometiff.girder_source", false]], "large_image_source_openjpeg": [[34, "module-large_image_source_openjpeg", false]], "large_image_source_openjpeg.girder_source": [[34, "module-large_image_source_openjpeg.girder_source", false]], "large_image_source_openslide": [[36, "module-large_image_source_openslide", false]], "large_image_source_openslide.girder_source": [[36, "module-large_image_source_openslide.girder_source", false]], "large_image_source_pil": [[38, "module-large_image_source_pil", false]], "large_image_source_pil.girder_source": [[38, "module-large_image_source_pil.girder_source", false]], "large_image_source_rasterio": [[40, "module-large_image_source_rasterio", false]], "large_image_source_rasterio.girder_source": [[40, "module-large_image_source_rasterio.girder_source", false]], "large_image_source_test": [[42, "module-large_image_source_test", false]], "large_image_source_tiff": [[44, "module-large_image_source_tiff", false]], "large_image_source_tiff.exceptions": [[44, "module-large_image_source_tiff.exceptions", false]], "large_image_source_tiff.girder_source": [[44, "module-large_image_source_tiff.girder_source", false]], "large_image_source_tiff.tiff_reader": [[44, "module-large_image_source_tiff.tiff_reader", false]], "large_image_source_tifffile": [[46, "module-large_image_source_tifffile", false]], "large_image_source_tifffile.girder_source": [[46, "module-large_image_source_tifffile.girder_source", false]], "large_image_source_vips": [[48, "module-large_image_source_vips", false]], "large_image_source_vips.girder_source": [[48, "module-large_image_source_vips.girder_source", false]], "large_image_source_zarr": [[50, "module-large_image_source_zarr", false]], "large_image_source_zarr.girder_source": [[50, "module-large_image_source_zarr.girder_source", false]], "large_image_tasks": [[52, "module-large_image_tasks", false]], "large_image_tasks.tasks": [[52, "module-large_image_tasks.tasks", false]], "largeimageannotationplugin (class in girder_large_image_annotation)": [[4, "girder_large_image_annotation.LargeImageAnnotationPlugin", false]], "largeimageplugin (class in girder_large_image)": [[0, "girder_large_image.LargeImagePlugin", false]], "largeimageresource (class in girder_large_image.rest.large_image_resource)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource", false]], "largeimagetasks (class in large_image_tasks)": [[52, "large_image_tasks.LargeImageTasks", false]], "launch_tile_server() (in module large_image.tilesource.jupyter)": [[11, "large_image.tilesource.jupyter.launch_tile_server", false]], "layer (large_image.tilesource.jupyter.map property)": [[11, "large_image.tilesource.jupyter.Map.layer", false]], "lazytiledict (class in large_image.tilesource.tiledict)": [[11, "large_image.tilesource.tiledict.LazyTileDict", false]], "levels (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.levels", false]], "levels (large_image_source_bioformats.girder_source.bioformatsgirdertilesource attribute)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource.levels", false]], "levels (large_image_source_deepzoom.girder_source.deepzoomgirdertilesource attribute)": [[17, "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource.levels", false]], "levels (large_image_source_dicom.girder_source.dicomgirdertilesource attribute)": [[19, "large_image_source_dicom.girder_source.DICOMGirderTileSource.levels", false]], "levels (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.levels", false]], "levels (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.levels", false]], "levels (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.levels", false]], "levels (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.levels", false]], "levels (large_image_source_multi.girder_source.multigirdertilesource attribute)": [[28, "large_image_source_multi.girder_source.MultiGirderTileSource.levels", false]], "levels (large_image_source_nd2.girder_source.nd2girdertilesource attribute)": [[30, "large_image_source_nd2.girder_source.ND2GirderTileSource.levels", false]], "levels (large_image_source_ometiff.girder_source.ometiffgirdertilesource attribute)": [[32, "large_image_source_ometiff.girder_source.OMETiffGirderTileSource.levels", false]], "levels (large_image_source_openjpeg.girder_source.openjpeggirdertilesource attribute)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource.levels", false]], "levels (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.levels", false]], "levels (large_image_source_pil.girder_source.pilgirdertilesource attribute)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.levels", false]], "levels (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.levels", false]], "levels (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.levels", false]], "levels (large_image_source_test.testtilesource attribute)": [[42, "large_image_source_test.TestTileSource.levels", false]], "levels (large_image_source_tiff.girder_source.tiffgirdertilesource attribute)": [[44, "large_image_source_tiff.girder_source.TiffGirderTileSource.levels", false]], "levels (large_image_source_tifffile.girder_source.tifffilegirdertilesource attribute)": [[46, "large_image_source_tifffile.girder_source.TifffileGirderTileSource.levels", false]], "levels (large_image_source_vips.girder_source.vipsgirdertilesource attribute)": [[48, "large_image_source_vips.girder_source.VipsGirderTileSource.levels", false]], "levels (large_image_source_zarr.girder_source.zarrgirdertilesource attribute)": [[50, "large_image_source_zarr.girder_source.ZarrGirderTileSource.levels", false]], "linestringtype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.linestringType", false]], "listextensions() (in module large_image.tilesource)": [[11, "large_image.tilesource.listExtensions", false]], "listmimetypes() (in module large_image.tilesource)": [[11, "large_image.tilesource.listMimeTypes", false]], "listsources() (girder_large_image.rest.large_image_resource.largeimageresource method)": [[2, "girder_large_image.rest.large_image_resource.LargeImageResource.listSources", false]], "listsources() (in module large_image.tilesource)": [[11, "large_image.tilesource.listSources", false]], "listtilesthumbnails() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.listTilesThumbnails", false]], "load() (girder_large_image.largeimageplugin method)": [[0, "girder_large_image.LargeImagePlugin.load", false]], "load() (girder_large_image_annotation.largeimageannotationplugin method)": [[4, "girder_large_image_annotation.LargeImageAnnotationPlugin.load", false]], "load() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.load", false]], "load() (in module large_image_source_dicom.assetstore)": [[20, "large_image_source_dicom.assetstore.load", false]], "load() (large_image_source_dicom.girder_plugin.dicomwebplugin method)": [[19, "large_image_source_dicom.girder_plugin.DICOMwebPlugin.load", false]], "loadcaches() (in module large_image.cache_util.cachefactory)": [[10, "large_image.cache_util.cachefactory.loadCaches", false]], "loadgirdertilesources() (in module girder_large_image.girder_tilesource)": [[0, "girder_large_image.girder_tilesource.loadGirderTileSources", false]], "loadmodel() (in module girder_large_image.loadmodelcache)": [[0, "girder_large_image.loadmodelcache.loadModel", false]], "logerror() (large_image.cache_util.base.basecache method)": [[10, "large_image.cache_util.base.BaseCache.logError", false]], "logged (large_image.cache_util.cachefactory attribute)": [[10, "large_image.cache_util.CacheFactory.logged", false]], "logged (large_image.cache_util.cachefactory.cachefactory attribute)": [[10, "large_image.cache_util.cachefactory.CacheFactory.logged", false]], "low (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.LOW", false]], "lower (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.LOWER", false]], "lrucachemetaclass (class in large_image.cache_util)": [[10, "large_image.cache_util.LruCacheMetaclass", false]], "lrucachemetaclass (class in large_image.cache_util.cache)": [[10, "large_image.cache_util.cache.LruCacheMetaclass", false]], "make_crs() (in module large_image_source_rasterio)": [[40, "large_image_source_rasterio.make_crs", false]], "make_layer() (large_image.tilesource.jupyter.map method)": [[11, "large_image.tilesource.jupyter.Map.make_layer", false]], "make_map() (large_image.tilesource.jupyter.map method)": [[11, "large_image.tilesource.jupyter.Map.make_map", false]], "make_vsi() (in module large_image.tilesource.geo)": [[11, "large_image.tilesource.geo.make_vsi", false]], "manual (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.MANUAL", false]], "map (class in large_image.tilesource.jupyter)": [[11, "large_image.tilesource.jupyter.Map", false]], "map (large_image.tilesource.jupyter.map property)": [[11, "large_image.tilesource.jupyter.Map.map", false]], "mapnikfiletilesource (class in large_image_source_mapnik)": [[26, "large_image_source_mapnik.MapnikFileTileSource", false]], "mapnikgirdertilesource (class in large_image_source_mapnik.girder_source)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource", false]], "maskpixelvalues() (in module large_image.tilesource.stylefuncs)": [[11, "large_image.tilesource.stylefuncs.maskPixelValues", false]], "maxannotationelements (girder_large_image_annotation.utils.plottableitemdata attribute)": [[7, "girder_large_image_annotation.utils.PlottableItemData.maxAnnotationElements", false]], "maxdistinct (girder_large_image_annotation.utils.plottableitemdata attribute)": [[7, "girder_large_image_annotation.utils.PlottableItemData.maxDistinct", false]], "maxitems (girder_large_image_annotation.utils.plottableitemdata attribute)": [[7, "girder_large_image_annotation.utils.PlottableItemData.maxItems", false]], "maxsize (large_image.cache_util.base.basecache property)": [[10, "large_image.cache_util.base.BaseCache.maxsize", false]], "maxsize (large_image.cache_util.memcache property)": [[10, "large_image.cache_util.MemCache.maxsize", false]], "maxsize (large_image.cache_util.memcache.memcache property)": [[10, "large_image.cache_util.memcache.MemCache.maxsize", false]], "maxsize (large_image.cache_util.rediscache property)": [[10, "large_image.cache_util.RedisCache.maxsize", false]], "maxsize (large_image.cache_util.rediscache.rediscache property)": [[10, "large_image.cache_util.rediscache.RedisCache.maxsize", false]], "mayhaveadjacentfiles() (girder_large_image.girder_tilesource.girdertilesource method)": [[0, "girder_large_image.girder_tilesource.GirderTileSource.mayHaveAdjacentFiles", false]], "mayhaveadjacentfiles() (large_image_source_bioformats.girder_source.bioformatsgirdertilesource method)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource.mayHaveAdjacentFiles", false]], "mayhaveadjacentfiles() (large_image_source_openjpeg.girder_source.openjpeggirdertilesource method)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource.mayHaveAdjacentFiles", false]], "medianfilter() (in module large_image.tilesource.stylefuncs)": [[11, "large_image.tilesource.stylefuncs.medianFilter", false]], "medium (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.MEDIUM", false]], "memcache (class in large_image.cache_util)": [[10, "large_image.cache_util.MemCache", false]], "memcache (class in large_image.cache_util.memcache)": [[10, "large_image.cache_util.memcache.MemCache", false]], "metadata (large_image.tilesource.base.tilesource property)": [[11, "large_image.tilesource.base.TileSource.metadata", false]], "metadata (large_image.tilesource.jupyter.map property)": [[11, "large_image.tilesource.jupyter.Map.metadata", false]], "metadata (large_image.tilesource.tilesource property)": [[11, "large_image.tilesource.TileSource.metadata", false]], "metadatasearchhandler() (in module girder_large_image)": [[0, "girder_large_image.metadataSearchHandler", false]], "metadatasearchhandler() (in module girder_large_image_annotation)": [[4, "girder_large_image_annotation.metadataSearchHandler", false]], "methodcache() (in module large_image.cache_util)": [[10, "large_image.cache_util.methodcache", false]], "methodcache() (in module large_image.cache_util.cache)": [[10, "large_image.cache_util.cache.methodcache", false]], "mimetype (large_image.tilesource.utilities.imagebytes property)": [[11, "large_image.tilesource.utilities.ImageBytes.mimetype", false]], "mimetypes (large_image.tilesource.base.tilesource attribute)": [[11, "large_image.tilesource.base.TileSource.mimeTypes", false]], "mimetypes (large_image.tilesource.geo.gdalbasefiletilesource attribute)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.mimeTypes", false]], "mimetypes (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.mimeTypes", false]], "mimetypes (large_image_source_bioformats.bioformatsfiletilesource attribute)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_deepzoom.deepzoomfiletilesource attribute)": [[17, "large_image_source_deepzoom.DeepzoomFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_dicom.dicomfiletilesource attribute)": [[19, "large_image_source_dicom.DICOMFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_multi.multifiletilesource attribute)": [[28, "large_image_source_multi.MultiFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_nd2.nd2filetilesource attribute)": [[30, "large_image_source_nd2.ND2FileTileSource.mimeTypes", false]], "mimetypes (large_image_source_ometiff.ometifffiletilesource attribute)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_openjpeg.openjpegfiletilesource attribute)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_openslide.openslidefiletilesource attribute)": [[36, "large_image_source_openslide.OpenslideFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_pil.pilfiletilesource attribute)": [[38, "large_image_source_pil.PILFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_tiff.tifffiletilesource attribute)": [[44, "large_image_source_tiff.TiffFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_tifffile.tifffilefiletilesource attribute)": [[46, "large_image_source_tifffile.TifffileFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_vips.vipsfiletilesource attribute)": [[48, "large_image_source_vips.VipsFileTileSource.mimeTypes", false]], "mimetypes (large_image_source_zarr.zarrfiletilesource attribute)": [[50, "large_image_source_zarr.ZarrFileTileSource.mimeTypes", false]], "mimetypeswithadjacentfiles (girder_large_image.girder_tilesource.girdertilesource attribute)": [[0, "girder_large_image.girder_tilesource.GirderTileSource.mimeTypesWithAdjacentFiles", false]], "mimetypeswithadjacentfiles (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.mimeTypesWithAdjacentFiles", false]], "minheight (large_image_source_vips.vipsfiletilesource property)": [[48, "large_image_source_vips.VipsFileTileSource.minHeight", false]], "minimizecaching() (in module large_image.config)": [[9, "large_image.config.minimizeCaching", false]], "minwidth (large_image_source_vips.vipsfiletilesource property)": [[48, "large_image_source_vips.VipsFileTileSource.minWidth", false]], "mm_x (large_image_source_vips.vipsfiletilesource property)": [[48, "large_image_source_vips.VipsFileTileSource.mm_x", false]], "mm_x (large_image_source_zarr.zarrfiletilesource property)": [[50, "large_image_source_zarr.ZarrFileTileSource.mm_x", false]], "mm_y (large_image_source_vips.vipsfiletilesource property)": [[48, "large_image_source_vips.VipsFileTileSource.mm_y", false]], "mm_y (large_image_source_zarr.zarrfiletilesource property)": [[50, "large_image_source_zarr.ZarrFileTileSource.mm_y", false]], "modify_tiff_before_write() (in module large_image_converter.format_aperio)": [[13, "large_image_converter.format_aperio.modify_tiff_before_write", false]], "modify_tiled_ifd() (in module large_image_converter.format_aperio)": [[13, "large_image_converter.format_aperio.modify_tiled_ifd", false]], "modify_vips_image_before_output() (in module large_image_converter.format_aperio)": [[13, "large_image_converter.format_aperio.modify_vips_image_before_output", false]], "module": [[0, "module-girder_large_image", false], [0, "module-girder_large_image.constants", false], [0, "module-girder_large_image.girder_tilesource", false], [0, "module-girder_large_image.loadmodelcache", false], [1, "module-girder_large_image.models", false], [1, "module-girder_large_image.models.image_item", false], [2, "module-girder_large_image.rest", false], [2, "module-girder_large_image.rest.item_meta", false], [2, "module-girder_large_image.rest.large_image_resource", false], [2, "module-girder_large_image.rest.tiles", false], [4, "module-girder_large_image_annotation", false], [4, "module-girder_large_image_annotation.constants", false], [4, "module-girder_large_image_annotation.handlers", false], [5, "module-girder_large_image_annotation.models", false], [5, "module-girder_large_image_annotation.models.annotation", false], [5, "module-girder_large_image_annotation.models.annotationelement", false], [6, "module-girder_large_image_annotation.rest", false], [6, "module-girder_large_image_annotation.rest.annotation", false], [7, "module-girder_large_image_annotation.utils", false], [9, "module-large_image", false], [9, "module-large_image.config", false], [9, "module-large_image.constants", false], [9, "module-large_image.exceptions", false], [10, "module-large_image.cache_util", false], [10, "module-large_image.cache_util.base", false], [10, "module-large_image.cache_util.cache", false], [10, "module-large_image.cache_util.cachefactory", false], [10, "module-large_image.cache_util.memcache", false], [10, "module-large_image.cache_util.rediscache", false], [11, "module-large_image.tilesource", false], [11, "module-large_image.tilesource.base", false], [11, "module-large_image.tilesource.geo", false], [11, "module-large_image.tilesource.jupyter", false], [11, "module-large_image.tilesource.resample", false], [11, "module-large_image.tilesource.stylefuncs", false], [11, "module-large_image.tilesource.tiledict", false], [11, "module-large_image.tilesource.tileiterator", false], [11, "module-large_image.tilesource.utilities", false], [13, "module-large_image_converter", false], [13, "module-large_image_converter.format_aperio", false], [15, "module-large_image_source_bioformats", false], [15, "module-large_image_source_bioformats.girder_source", false], [17, "module-large_image_source_deepzoom", false], [17, "module-large_image_source_deepzoom.girder_source", false], [19, "module-large_image_source_dicom", false], [19, "module-large_image_source_dicom.dicom_metadata", false], [19, "module-large_image_source_dicom.dicom_tags", false], [19, "module-large_image_source_dicom.dicomweb_utils", false], [19, "module-large_image_source_dicom.girder_plugin", false], [19, "module-large_image_source_dicom.girder_source", false], [20, "module-large_image_source_dicom.assetstore", false], [20, "module-large_image_source_dicom.assetstore.dicomweb_assetstore_adapter", false], [20, "module-large_image_source_dicom.assetstore.rest", false], [22, "module-large_image_source_dummy", false], [24, "module-large_image_source_gdal", false], [24, "module-large_image_source_gdal.girder_source", false], [26, "module-large_image_source_mapnik", false], [26, "module-large_image_source_mapnik.girder_source", false], [28, "module-large_image_source_multi", false], [28, "module-large_image_source_multi.girder_source", false], [30, "module-large_image_source_nd2", false], [30, "module-large_image_source_nd2.girder_source", false], [32, "module-large_image_source_ometiff", false], [32, "module-large_image_source_ometiff.girder_source", false], [34, "module-large_image_source_openjpeg", false], [34, "module-large_image_source_openjpeg.girder_source", false], [36, "module-large_image_source_openslide", false], [36, "module-large_image_source_openslide.girder_source", false], [38, "module-large_image_source_pil", false], [38, "module-large_image_source_pil.girder_source", false], [40, "module-large_image_source_rasterio", false], [40, "module-large_image_source_rasterio.girder_source", false], [42, "module-large_image_source_test", false], [44, "module-large_image_source_tiff", false], [44, "module-large_image_source_tiff.exceptions", false], [44, "module-large_image_source_tiff.girder_source", false], [44, "module-large_image_source_tiff.tiff_reader", false], [46, "module-large_image_source_tifffile", false], [46, "module-large_image_source_tifffile.girder_source", false], [48, "module-large_image_source_vips", false], [48, "module-large_image_source_vips.girder_source", false], [50, "module-large_image_source_zarr", false], [50, "module-large_image_source_zarr.girder_source", false], [52, "module-large_image_tasks", false], [52, "module-large_image_tasks.tasks", false]], "multifiletilesource (class in large_image_source_multi)": [[28, "large_image_source_multi.MultiFileTileSource", false]], "multigirdertilesource (class in large_image_source_multi.girder_source)": [[28, "large_image_source_multi.girder_source.MultiGirderTileSource", false]], "multilinestringtype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.multilinestringType", false]], "multipointtype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.multipointType", false]], "multipolygontype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.multipolygonType", false]], "name (large_image.tilesource.base.tilesource attribute)": [[11, "large_image.tilesource.base.TileSource.name", false]], "name (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.name", false]], "name (large_image_source_bioformats.bioformatsfiletilesource attribute)": [[15, "large_image_source_bioformats.BioformatsFileTileSource.name", false]], "name (large_image_source_bioformats.girder_source.bioformatsgirdertilesource attribute)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource.name", false]], "name (large_image_source_deepzoom.deepzoomfiletilesource attribute)": [[17, "large_image_source_deepzoom.DeepzoomFileTileSource.name", false]], "name (large_image_source_deepzoom.girder_source.deepzoomgirdertilesource attribute)": [[17, "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource.name", false]], "name (large_image_source_dicom.dicomfiletilesource attribute)": [[19, "large_image_source_dicom.DICOMFileTileSource.name", false]], "name (large_image_source_dicom.girder_source.dicomgirdertilesource attribute)": [[19, "large_image_source_dicom.girder_source.DICOMGirderTileSource.name", false]], "name (large_image_source_dummy.dummytilesource attribute)": [[22, "large_image_source_dummy.DummyTileSource.name", false]], "name (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.name", false]], "name (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.name", false]], "name (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.name", false]], "name (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.name", false]], "name (large_image_source_multi.girder_source.multigirdertilesource attribute)": [[28, "large_image_source_multi.girder_source.MultiGirderTileSource.name", false]], "name (large_image_source_multi.multifiletilesource attribute)": [[28, "large_image_source_multi.MultiFileTileSource.name", false]], "name (large_image_source_nd2.girder_source.nd2girdertilesource attribute)": [[30, "large_image_source_nd2.girder_source.ND2GirderTileSource.name", false]], "name (large_image_source_nd2.nd2filetilesource attribute)": [[30, "large_image_source_nd2.ND2FileTileSource.name", false]], "name (large_image_source_ometiff.girder_source.ometiffgirdertilesource attribute)": [[32, "large_image_source_ometiff.girder_source.OMETiffGirderTileSource.name", false]], "name (large_image_source_ometiff.ometifffiletilesource attribute)": [[32, "large_image_source_ometiff.OMETiffFileTileSource.name", false]], "name (large_image_source_openjpeg.girder_source.openjpeggirdertilesource attribute)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource.name", false]], "name (large_image_source_openjpeg.openjpegfiletilesource attribute)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource.name", false]], "name (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.name", false]], "name (large_image_source_openslide.openslidefiletilesource attribute)": [[36, "large_image_source_openslide.OpenslideFileTileSource.name", false]], "name (large_image_source_pil.girder_source.pilgirdertilesource attribute)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.name", false]], "name (large_image_source_pil.pilfiletilesource attribute)": [[38, "large_image_source_pil.PILFileTileSource.name", false]], "name (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.name", false]], "name (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.name", false]], "name (large_image_source_test.testtilesource attribute)": [[42, "large_image_source_test.TestTileSource.name", false]], "name (large_image_source_tiff.girder_source.tiffgirdertilesource attribute)": [[44, "large_image_source_tiff.girder_source.TiffGirderTileSource.name", false]], "name (large_image_source_tiff.tifffiletilesource attribute)": [[44, "large_image_source_tiff.TiffFileTileSource.name", false]], "name (large_image_source_tifffile.girder_source.tifffilegirdertilesource attribute)": [[46, "large_image_source_tifffile.girder_source.TifffileGirderTileSource.name", false]], "name (large_image_source_tifffile.tifffilefiletilesource attribute)": [[46, "large_image_source_tifffile.TifffileFileTileSource.name", false]], "name (large_image_source_vips.girder_source.vipsgirdertilesource attribute)": [[48, "large_image_source_vips.girder_source.VipsGirderTileSource.name", false]], "name (large_image_source_vips.vipsfiletilesource attribute)": [[48, "large_image_source_vips.VipsFileTileSource.name", false]], "name (large_image_source_zarr.girder_source.zarrgirdertilesource attribute)": [[50, "large_image_source_zarr.girder_source.ZarrGirderTileSource.name", false]], "name (large_image_source_zarr.zarrfiletilesource attribute)": [[50, "large_image_source_zarr.ZarrFileTileSource.name", false]], "named (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.NAMED", false]], "namedcaches (large_image.cache_util.cache.lrucachemetaclass attribute)": [[10, "large_image.cache_util.cache.LruCacheMetaclass.namedCaches", false]], "namedcaches (large_image.cache_util.lrucachemetaclass attribute)": [[10, "large_image.cache_util.LruCacheMetaclass.namedCaches", false]], "namedtupletodict() (in module large_image_source_nd2)": [[30, "large_image_source_nd2.namedtupleToDict", false]], "namematches (large_image.tilesource.base.tilesource attribute)": [[11, "large_image.tilesource.base.TileSource.nameMatches", false]], "namematches (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.nameMatches", false]], "namematches (large_image_source_dicom.dicomfiletilesource attribute)": [[19, "large_image_source_dicom.DICOMFileTileSource.nameMatches", false]], "nd2filetilesource (class in large_image_source_nd2)": [[30, "large_image_source_nd2.ND2FileTileSource", false]], "nd2girdertilesource (class in large_image_source_nd2.girder_source)": [[30, "large_image_source_nd2.girder_source.ND2GirderTileSource", false]], "nearpoweroftwo() (in module large_image.tilesource)": [[11, "large_image.tilesource.nearPowerOfTwo", false]], "nearpoweroftwo() (in module large_image.tilesource.utilities)": [[11, "large_image.tilesource.utilities.nearPowerOfTwo", false]], "new() (in module large_image.tilesource)": [[11, "large_image.tilesource.new", false]], "new() (in module large_image_source_vips)": [[48, "large_image_source_vips.new", false]], "new() (in module large_image_source_zarr)": [[50, "large_image_source_zarr.new", false]], "newpriority (large_image.tilesource.base.tilesource attribute)": [[11, "large_image.tilesource.base.TileSource.newPriority", false]], "newpriority (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.newPriority", false]], "newpriority (large_image_source_vips.vipsfiletilesource attribute)": [[48, "large_image_source_vips.VipsFileTileSource.newPriority", false]], "newpriority (large_image_source_zarr.zarrfiletilesource attribute)": [[50, "large_image_source_zarr.ZarrFileTileSource.newPriority", false]], "novice (girder_large_image_annotation.models.annotation.annotation.skill attribute)": [[5, "girder_large_image_annotation.models.annotation.Annotation.Skill.NOVICE", false]], "np_max (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.NP_MAX", false]], "np_max_color (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.NP_MAX_COLOR", false]], "np_mean (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.NP_MEAN", false]], "np_median (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.NP_MEDIAN", false]], "np_min (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.NP_MIN", false]], "np_min_color (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.NP_MIN_COLOR", false]], "np_mode (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.NP_MODE", false]], "np_nearest (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.NP_NEAREST", false]], "numberinstance (girder_large_image_annotation.models.annotation.annotation attribute)": [[5, "girder_large_image_annotation.models.annotation.Annotation.numberInstance", false]], "numpyresize() (in module large_image.tilesource.resample)": [[11, "large_image.tilesource.resample.numpyResize", false]], "ometifffiletilesource (class in large_image_source_ometiff)": [[32, "large_image_source_ometiff.OMETiffFileTileSource", false]], "ometiffgirdertilesource (class in large_image_source_ometiff.girder_source)": [[32, "large_image_source_ometiff.girder_source.OMETiffGirderTileSource", false]], "open() (in module large_image.tilesource)": [[11, "large_image.tilesource.open", false]], "open() (in module large_image_source_bioformats)": [[15, "large_image_source_bioformats.open", false]], "open() (in module large_image_source_deepzoom)": [[17, "large_image_source_deepzoom.open", false]], "open() (in module large_image_source_dicom)": [[19, "large_image_source_dicom.open", false]], "open() (in module large_image_source_dummy)": [[22, "large_image_source_dummy.open", false]], "open() (in module large_image_source_gdal)": [[24, "large_image_source_gdal.open", false]], "open() (in module large_image_source_mapnik)": [[26, "large_image_source_mapnik.open", false]], "open() (in module large_image_source_multi)": [[28, "large_image_source_multi.open", false]], "open() (in module large_image_source_nd2)": [[30, "large_image_source_nd2.open", false]], "open() (in module large_image_source_ometiff)": [[32, "large_image_source_ometiff.open", false]], "open() (in module large_image_source_openjpeg)": [[34, "large_image_source_openjpeg.open", false]], "open() (in module large_image_source_openslide)": [[36, "large_image_source_openslide.open", false]], "open() (in module large_image_source_pil)": [[38, "large_image_source_pil.open", false]], "open() (in module large_image_source_rasterio)": [[40, "large_image_source_rasterio.open", false]], "open() (in module large_image_source_test)": [[42, "large_image_source_test.open", false]], "open() (in module large_image_source_tiff)": [[44, "large_image_source_tiff.open", false]], "open() (in module large_image_source_tifffile)": [[46, "large_image_source_tifffile.open", false]], "open() (in module large_image_source_vips)": [[48, "large_image_source_vips.open", false]], "open() (in module large_image_source_zarr)": [[50, "large_image_source_zarr.open", false]], "openjpegfiletilesource (class in large_image_source_openjpeg)": [[34, "large_image_source_openjpeg.OpenjpegFileTileSource", false]], "openjpeggirdertilesource (class in large_image_source_openjpeg.girder_source)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource", false]], "openslidefiletilesource (class in large_image_source_openslide)": [[36, "large_image_source_openslide.OpenslideFileTileSource", false]], "openslidegirdertilesource (class in large_image_source_openslide.girder_source)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource", false]], "origin (large_image_source_vips.vipsfiletilesource property)": [[48, "large_image_source_vips.VipsFileTileSource.origin", false]], "overlayschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.overlaySchema", false]], "parse_image_description() (large_image_source_tiff.tiff_reader.tiledtiffdirectory method)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory.parse_image_description", false]], "patchlibtiff() (in module large_image_source_tiff.tiff_reader)": [[44, "large_image_source_tiff.tiff_reader.patchLibtiff", false]], "patchmount() (in module girder_large_image)": [[0, "girder_large_image.patchMount", false]], "pickavailablecache() (in module large_image.cache_util)": [[10, "large_image.cache_util.pickAvailableCache", false]], "pickavailablecache() (in module large_image.cache_util.cachefactory)": [[10, "large_image.cache_util.cachefactory.pickAvailableCache", false]], "pil_bicubic (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.PIL_BICUBIC", false]], "pil_bilinear (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.PIL_BILINEAR", false]], "pil_box (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.PIL_BOX", false]], "pil_hamming (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.PIL_HAMMING", false]], "pil_lanczos (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.PIL_LANCZOS", false]], "pil_max_enum (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.PIL_MAX_ENUM", false]], "pil_nearest (large_image.tilesource.resample.resamplemethod attribute)": [[11, "large_image.tilesource.resample.ResampleMethod.PIL_NEAREST", false]], "pilfiletilesource (class in large_image_source_pil)": [[38, "large_image_source_pil.PILFileTileSource", false]], "pilgirdertilesource (class in large_image_source_pil.girder_source)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource", false]], "pilresize() (in module large_image.tilesource.resample)": [[11, "large_image.tilesource.resample.pilResize", false]], "pixelinfo (large_image_source_tiff.tiff_reader.tiledtiffdirectory property)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory.pixelInfo", false]], "pixelmapcategoryschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.pixelmapCategorySchema", false]], "pixelmapschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.pixelmapSchema", false]], "pixeltoprojection() (large_image.tilesource.geo.gdalbasefiletilesource method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.pixelToProjection", false]], "pixeltoprojection() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.pixelToProjection", false]], "pixeltoprojection() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.pixelToProjection", false]], "plottableitemdata (class in girder_large_image_annotation.utils)": [[7, "girder_large_image_annotation.utils.PlottableItemData", false]], "pluginsettings (class in girder_large_image.constants)": [[0, "girder_large_image.constants.PluginSettings", false]], "pointshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.pointShapeSchema", false]], "pointtype() (girder_large_image_annotation.utils.annotationgeojson method)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON.pointType", false]], "pointtype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.pointType", false]], "polygontype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.polygonType", false]], "polylineshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.polylineShapeSchema", false]], "polylinetype() (girder_large_image_annotation.utils.annotationgeojson method)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON.polylineType", false]], "polylinetype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.polylineType", false]], "preferred (large_image.constants.sourcepriority attribute)": [[9, "large_image.constants.SourcePriority.PREFERRED", false]], "preparecopyitem() (in module girder_large_image)": [[0, "girder_large_image.prepareCopyItem", false]], "process_annotations() (in module girder_large_image_annotation.handlers)": [[4, "girder_large_image_annotation.handlers.process_annotations", false]], "projection (large_image.tilesource.geo.gdalbasefiletilesource attribute)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.projection", false]], "projection (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.projection", false]], "projection (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.projection", false]], "projection (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.projection", false]], "projection (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.projection", false]], "projection (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.projection", false]], "projection (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.projection", false]], "projectionorigin (large_image.tilesource.geo.gdalbasefiletilesource attribute)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.projectionOrigin", false]], "projectionorigin (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.projectionOrigin", false]], "projectionorigin (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.projectionOrigin", false]], "projectionorigin (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.projectionOrigin", false]], "projectionorigin (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.projectionOrigin", false]], "projectionorigin (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.projectionOrigin", false]], "projectionorigin (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.projectionOrigin", false]], "putyamlconfigfile() (in module girder_large_image.rest)": [[2, "girder_large_image.rest.putYAMLConfigFile", false]], "rangevalueschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.rangeValueSchema", false]], "rasteriofiletilesource (class in large_image_source_rasterio)": [[40, "large_image_source_rasterio.RasterioFileTileSource", false]], "rasteriogirdertilesource (class in large_image_source_rasterio.girder_source)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource", false]], "recordselector() (girder_large_image_annotation.utils.plottableitemdata static method)": [[7, "girder_large_image_annotation.utils.PlottableItemData.recordSelector", false]], "rectanglegridshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.rectangleGridShapeSchema", false]], "rectangleshapeschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.rectangleShapeSchema", false]], "rectangletype() (girder_large_image_annotation.utils.annotationgeojson method)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON.rectangleType", false]], "rectangletype() (girder_large_image_annotation.utils.geojsonannotation method)": [[7, "girder_large_image_annotation.utils.GeoJSONAnnotation.rectangleType", false]], "rediscache (class in large_image.cache_util)": [[10, "large_image.cache_util.RedisCache", false]], "rediscache (class in large_image.cache_util.rediscache)": [[10, "large_image.cache_util.rediscache.RedisCache", false]], "release() (large_image.tilesource.tiledict.lazytiledict method)": [[11, "large_image.tilesource.tiledict.LazyTileDict.release", false]], "remove() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.remove", false]], "removeelements() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.removeElements", false]], "removeoldannotations() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.removeOldAnnotations", false]], "removeoldelements() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.removeOldElements", false]], "removethumbnailfiles() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.removeThumbnailFiles", false]], "removethumbnails() (in module girder_large_image)": [[0, "girder_large_image.removeThumbnails", false]], "removewithquery() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.removeWithQuery", false]], "resamplemethod (class in large_image.tilesource.resample)": [[11, "large_image.tilesource.resample.ResampleMethod", false]], "resolveannotationgirderids() (in module girder_large_image_annotation.handlers)": [[4, "girder_large_image_annotation.handlers.resolveAnnotationGirderIds", false]], "returnfolderannotations() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.returnFolderAnnotations", false]], "revertannotationhistory() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.revertAnnotationHistory", false]], "revertversion() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.revertVersion", false]], "rotate() (girder_large_image_annotation.utils.annotationgeojson method)": [[7, "girder_large_image_annotation.utils.AnnotationGeoJSON.rotate", false]], "save() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.save", false]], "saveelementasfile() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.saveElementAsFile", false]], "setaccesslist() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.setAccessList", false]], "setconfig() (in module large_image.config)": [[9, "large_image.config.setConfig", false]], "setcontentheaders() (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.setContentHeaders", false]], "setcontentheaders() (large_image_source_dicom.assetstore.dicomwebassetstoreadapter method)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.setContentHeaders", false]], "setfolderannotationaccess() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.setFolderAnnotationAccess", false]], "setformat() (large_image.tilesource.tiledict.lazytiledict method)": [[11, "large_image.tilesource.tiledict.LazyTileDict.setFormat", false]], "setmetadata() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.setMetadata", false]], "setmetadata() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.setMetadata", false]], "sizex (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.sizeX", false]], "sizex (large_image_source_bioformats.girder_source.bioformatsgirdertilesource attribute)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource.sizeX", false]], "sizex (large_image_source_deepzoom.girder_source.deepzoomgirdertilesource attribute)": [[17, "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource.sizeX", false]], "sizex (large_image_source_dicom.girder_source.dicomgirdertilesource attribute)": [[19, "large_image_source_dicom.girder_source.DICOMGirderTileSource.sizeX", false]], "sizex (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.sizeX", false]], "sizex (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.sizeX", false]], "sizex (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.sizeX", false]], "sizex (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.sizeX", false]], "sizex (large_image_source_multi.girder_source.multigirdertilesource attribute)": [[28, "large_image_source_multi.girder_source.MultiGirderTileSource.sizeX", false]], "sizex (large_image_source_nd2.girder_source.nd2girdertilesource attribute)": [[30, "large_image_source_nd2.girder_source.ND2GirderTileSource.sizeX", false]], "sizex (large_image_source_ometiff.girder_source.ometiffgirdertilesource attribute)": [[32, "large_image_source_ometiff.girder_source.OMETiffGirderTileSource.sizeX", false]], "sizex (large_image_source_openjpeg.girder_source.openjpeggirdertilesource attribute)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource.sizeX", false]], "sizex (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.sizeX", false]], "sizex (large_image_source_pil.girder_source.pilgirdertilesource attribute)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.sizeX", false]], "sizex (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.sizeX", false]], "sizex (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.sizeX", false]], "sizex (large_image_source_test.testtilesource attribute)": [[42, "large_image_source_test.TestTileSource.sizeX", false]], "sizex (large_image_source_tiff.girder_source.tiffgirdertilesource attribute)": [[44, "large_image_source_tiff.girder_source.TiffGirderTileSource.sizeX", false]], "sizex (large_image_source_tifffile.girder_source.tifffilegirdertilesource attribute)": [[46, "large_image_source_tifffile.girder_source.TifffileGirderTileSource.sizeX", false]], "sizex (large_image_source_vips.girder_source.vipsgirdertilesource attribute)": [[48, "large_image_source_vips.girder_source.VipsGirderTileSource.sizeX", false]], "sizex (large_image_source_zarr.girder_source.zarrgirdertilesource attribute)": [[50, "large_image_source_zarr.girder_source.ZarrGirderTileSource.sizeX", false]], "sizey (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.sizeY", false]], "sizey (large_image_source_bioformats.girder_source.bioformatsgirdertilesource attribute)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource.sizeY", false]], "sizey (large_image_source_deepzoom.girder_source.deepzoomgirdertilesource attribute)": [[17, "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource.sizeY", false]], "sizey (large_image_source_dicom.girder_source.dicomgirdertilesource attribute)": [[19, "large_image_source_dicom.girder_source.DICOMGirderTileSource.sizeY", false]], "sizey (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.sizeY", false]], "sizey (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.sizeY", false]], "sizey (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.sizeY", false]], "sizey (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.sizeY", false]], "sizey (large_image_source_multi.girder_source.multigirdertilesource attribute)": [[28, "large_image_source_multi.girder_source.MultiGirderTileSource.sizeY", false]], "sizey (large_image_source_nd2.girder_source.nd2girdertilesource attribute)": [[30, "large_image_source_nd2.girder_source.ND2GirderTileSource.sizeY", false]], "sizey (large_image_source_ometiff.girder_source.ometiffgirdertilesource attribute)": [[32, "large_image_source_ometiff.girder_source.OMETiffGirderTileSource.sizeY", false]], "sizey (large_image_source_openjpeg.girder_source.openjpeggirdertilesource attribute)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource.sizeY", false]], "sizey (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.sizeY", false]], "sizey (large_image_source_pil.girder_source.pilgirdertilesource attribute)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.sizeY", false]], "sizey (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.sizeY", false]], "sizey (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.sizeY", false]], "sizey (large_image_source_test.testtilesource attribute)": [[42, "large_image_source_test.TestTileSource.sizeY", false]], "sizey (large_image_source_tiff.girder_source.tiffgirdertilesource attribute)": [[44, "large_image_source_tiff.girder_source.TiffGirderTileSource.sizeY", false]], "sizey (large_image_source_tifffile.girder_source.tifffilegirdertilesource attribute)": [[46, "large_image_source_tifffile.girder_source.TifffileGirderTileSource.sizeY", false]], "sizey (large_image_source_vips.girder_source.vipsgirdertilesource attribute)": [[48, "large_image_source_vips.girder_source.VipsGirderTileSource.sizeY", false]], "sizey (large_image_source_zarr.girder_source.zarrgirdertilesource attribute)": [[50, "large_image_source_zarr.girder_source.ZarrGirderTileSource.sizeY", false]], "sourcelevels (large_image.tilesource.geo.gdalbasefiletilesource attribute)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.sourceLevels", false]], "sourcelevels (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.sourceLevels", false]], "sourcelevels (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.sourceLevels", false]], "sourcelevels (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.sourceLevels", false]], "sourcelevels (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.sourceLevels", false]], "sourcelevels (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.sourceLevels", false]], "sourcelevels (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.sourceLevels", false]], "sourcepriority (class in large_image.constants)": [[9, "large_image.constants.SourcePriority", false]], "sourcesizex (large_image.tilesource.geo.gdalbasefiletilesource attribute)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.sourceSizeX", false]], "sourcesizex (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.sourceSizeX", false]], "sourcesizex (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.sourceSizeX", false]], "sourcesizex (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.sourceSizeX", false]], "sourcesizex (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.sourceSizeX", false]], "sourcesizex (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.sourceSizeX", false]], "sourcesizex (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.sourceSizeX", false]], "sourcesizey (large_image.tilesource.geo.gdalbasefiletilesource attribute)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.sourceSizeY", false]], "sourcesizey (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.sourceSizeY", false]], "sourcesizey (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.sourceSizeY", false]], "sourcesizey (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.sourceSizeY", false]], "sourcesizey (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.sourceSizeY", false]], "sourcesizey (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.sourceSizeY", false]], "sourcesizey (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.sourceSizeY", false]], "strhash() (in module large_image.cache_util)": [[10, "large_image.cache_util.strhash", false]], "strhash() (in module large_image.cache_util.cache)": [[10, "large_image.cache_util.cache.strhash", false]], "style (large_image.tilesource.base.tilesource property)": [[11, "large_image.tilesource.base.TileSource.style", false]], "style (large_image.tilesource.tilesource property)": [[11, "large_image.tilesource.TileSource.style", false]], "task_imports() (large_image_tasks.largeimagetasks method)": [[52, "large_image_tasks.LargeImageTasks.task_imports", false]], "testtilesource (class in large_image_source_test)": [[42, "large_image_source_test.TestTileSource", false]], "tifferror": [[44, "large_image_source_tiff.exceptions.TiffError", false]], "tifffilefiletilesource (class in large_image_source_tifffile)": [[46, "large_image_source_tifffile.TifffileFileTileSource", false]], "tifffilegirdertilesource (class in large_image_source_tifffile.girder_source)": [[46, "large_image_source_tifffile.girder_source.TifffileGirderTileSource", false]], "tifffiletilesource (class in large_image_source_tiff)": [[44, "large_image_source_tiff.TiffFileTileSource", false]], "tiffgirdertilesource (class in large_image_source_tiff.girder_source)": [[44, "large_image_source_tiff.girder_source.TiffGirderTileSource", false]], "tilecacheconfigurationerror": [[9, "large_image.exceptions.TileCacheConfigurationError", false]], "tilecacheerror": [[9, "large_image.exceptions.TileCacheError", false]], "tiledtiffdirectory (class in large_image_source_tiff.tiff_reader)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory", false]], "tileframes() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.tileFrames", false]], "tileframes() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.tileFrames", false]], "tileframes() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.tileFrames", false]], "tileframes() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.tileFrames", false]], "tileframesquadinfo() (girder_large_image.rest.tiles.tilesitemresource method)": [[2, "girder_large_image.rest.tiles.TilesItemResource.tileFramesQuadInfo", false]], "tilegeneralerror": [[9, "large_image.exceptions.TileGeneralError", false], [11, "large_image.tilesource.TileGeneralError", false]], "tilegeneralexception (in module large_image.exceptions)": [[9, "large_image.exceptions.TileGeneralException", false]], "tilegeneralexception (in module large_image.tilesource)": [[11, "large_image.tilesource.TileGeneralException", false]], "tileheight (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.tileHeight", false]], "tileheight (large_image_source_bioformats.girder_source.bioformatsgirdertilesource attribute)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource.tileHeight", false]], "tileheight (large_image_source_deepzoom.girder_source.deepzoomgirdertilesource attribute)": [[17, "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource.tileHeight", false]], "tileheight (large_image_source_dicom.girder_source.dicomgirdertilesource attribute)": [[19, "large_image_source_dicom.girder_source.DICOMGirderTileSource.tileHeight", false]], "tileheight (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.tileHeight", false]], "tileheight (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.tileHeight", false]], "tileheight (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.tileHeight", false]], "tileheight (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.tileHeight", false]], "tileheight (large_image_source_multi.girder_source.multigirdertilesource attribute)": [[28, "large_image_source_multi.girder_source.MultiGirderTileSource.tileHeight", false]], "tileheight (large_image_source_nd2.girder_source.nd2girdertilesource attribute)": [[30, "large_image_source_nd2.girder_source.ND2GirderTileSource.tileHeight", false]], "tileheight (large_image_source_ometiff.girder_source.ometiffgirdertilesource attribute)": [[32, "large_image_source_ometiff.girder_source.OMETiffGirderTileSource.tileHeight", false]], "tileheight (large_image_source_openjpeg.girder_source.openjpeggirdertilesource attribute)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource.tileHeight", false]], "tileheight (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.tileHeight", false]], "tileheight (large_image_source_pil.girder_source.pilgirdertilesource attribute)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.tileHeight", false]], "tileheight (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.tileHeight", false]], "tileheight (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.tileHeight", false]], "tileheight (large_image_source_test.testtilesource attribute)": [[42, "large_image_source_test.TestTileSource.tileHeight", false]], "tileheight (large_image_source_tiff.girder_source.tiffgirdertilesource attribute)": [[44, "large_image_source_tiff.girder_source.TiffGirderTileSource.tileHeight", false]], "tileheight (large_image_source_tiff.tiff_reader.tiledtiffdirectory property)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory.tileHeight", false]], "tileheight (large_image_source_tifffile.girder_source.tifffilegirdertilesource attribute)": [[46, "large_image_source_tifffile.girder_source.TifffileGirderTileSource.tileHeight", false]], "tileheight (large_image_source_vips.girder_source.vipsgirdertilesource attribute)": [[48, "large_image_source_vips.girder_source.VipsGirderTileSource.tileHeight", false]], "tileheight (large_image_source_zarr.girder_source.zarrgirdertilesource attribute)": [[50, "large_image_source_zarr.girder_source.ZarrGirderTileSource.tileHeight", false]], "tileiterator (class in large_image.tilesource.tileiterator)": [[11, "large_image.tilesource.tileiterator.TileIterator", false]], "tileiterator() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.tileIterator", false]], "tileiterator() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.tileIterator", false]], "tileiteratoratanotherscale() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.tileIteratorAtAnotherScale", false]], "tileiteratoratanotherscale() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.tileIteratorAtAnotherScale", false]], "tilesitemresource (class in girder_large_image.rest.tiles)": [[2, "girder_large_image.rest.tiles.TilesItemResource", false]], "tilesource (class in large_image.tilesource)": [[11, "large_image.tilesource.TileSource", false]], "tilesource (class in large_image.tilesource.base)": [[11, "large_image.tilesource.base.TileSource", false]], "tilesource() (girder_large_image.models.image_item.imageitem method)": [[1, "girder_large_image.models.image_item.ImageItem.tileSource", false]], "tilesourceassetstoreerror": [[9, "large_image.exceptions.TileSourceAssetstoreError", false], [11, "large_image.tilesource.TileSourceAssetstoreError", false]], "tilesourceassetstoreexception (in module large_image.exceptions)": [[9, "large_image.exceptions.TileSourceAssetstoreException", false]], "tilesourceassetstoreexception (in module large_image.tilesource)": [[11, "large_image.tilesource.TileSourceAssetstoreException", false]], "tilesourceerror": [[9, "large_image.exceptions.TileSourceError", false], [11, "large_image.tilesource.TileSourceError", false]], "tilesourceexception (in module large_image.exceptions)": [[9, "large_image.exceptions.TileSourceException", false]], "tilesourceexception (in module large_image.tilesource)": [[11, "large_image.tilesource.TileSourceException", false]], "tilesourcefilenotfounderror": [[9, "large_image.exceptions.TileSourceFileNotFoundError", false], [11, "large_image.tilesource.TileSourceFileNotFoundError", false]], "tilesourceinefficienterror": [[9, "large_image.exceptions.TileSourceInefficientError", false]], "tilesourcexyzrangeerror": [[9, "large_image.exceptions.TileSourceXYZRangeError", false]], "tilewidth (large_image.tilesource.tilesource attribute)": [[11, "large_image.tilesource.TileSource.tileWidth", false]], "tilewidth (large_image_source_bioformats.girder_source.bioformatsgirdertilesource attribute)": [[15, "large_image_source_bioformats.girder_source.BioformatsGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_deepzoom.girder_source.deepzoomgirdertilesource attribute)": [[17, "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_dicom.girder_source.dicomgirdertilesource attribute)": [[19, "large_image_source_dicom.girder_source.DICOMGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.tileWidth", false]], "tilewidth (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.tileWidth", false]], "tilewidth (large_image_source_multi.girder_source.multigirdertilesource attribute)": [[28, "large_image_source_multi.girder_source.MultiGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_nd2.girder_source.nd2girdertilesource attribute)": [[30, "large_image_source_nd2.girder_source.ND2GirderTileSource.tileWidth", false]], "tilewidth (large_image_source_ometiff.girder_source.ometiffgirdertilesource attribute)": [[32, "large_image_source_ometiff.girder_source.OMETiffGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_openjpeg.girder_source.openjpeggirdertilesource attribute)": [[34, "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_openslide.girder_source.openslidegirdertilesource attribute)": [[36, "large_image_source_openslide.girder_source.OpenslideGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_pil.girder_source.pilgirdertilesource attribute)": [[38, "large_image_source_pil.girder_source.PILGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.tileWidth", false]], "tilewidth (large_image_source_test.testtilesource attribute)": [[42, "large_image_source_test.TestTileSource.tileWidth", false]], "tilewidth (large_image_source_tiff.girder_source.tiffgirdertilesource attribute)": [[44, "large_image_source_tiff.girder_source.TiffGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_tiff.tiff_reader.tiledtiffdirectory property)": [[44, "large_image_source_tiff.tiff_reader.TiledTiffDirectory.tileWidth", false]], "tilewidth (large_image_source_tifffile.girder_source.tifffilegirdertilesource attribute)": [[46, "large_image_source_tifffile.girder_source.TifffileGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_vips.girder_source.vipsgirdertilesource attribute)": [[48, "large_image_source_vips.girder_source.VipsGirderTileSource.tileWidth", false]], "tilewidth (large_image_source_zarr.girder_source.zarrgirdertilesource attribute)": [[50, "large_image_source_zarr.girder_source.ZarrGirderTileSource.tileWidth", false]], "to_map() (large_image.tilesource.jupyter.map method)": [[11, "large_image.tilesource.jupyter.Map.to_map", false]], "tonativepixelcoordinates() (large_image.tilesource.geo.gdalbasefiletilesource method)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.toNativePixelCoordinates", false]], "tonativepixelcoordinates() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.toNativePixelCoordinates", false]], "tonativepixelcoordinates() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.toNativePixelCoordinates", false]], "total_memory() (in module large_image.config)": [[9, "large_image.config.total_memory", false]], "transformarray (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.transformArray", false]], "unbindgirdereventsbyhandlername() (in module girder_large_image)": [[0, "girder_large_image.unbindGirderEventsByHandlerName", false]], "unitsacrosslevel0 (large_image.tilesource.geo.gdalbasefiletilesource attribute)": [[11, "large_image.tilesource.geo.GDALBaseFileTileSource.unitsAcrossLevel0", false]], "unitsacrosslevel0 (large_image_source_gdal.gdalfiletilesource attribute)": [[24, "large_image_source_gdal.GDALFileTileSource.unitsAcrossLevel0", false]], "unitsacrosslevel0 (large_image_source_gdal.girder_source.gdalgirdertilesource attribute)": [[24, "large_image_source_gdal.girder_source.GDALGirderTileSource.unitsAcrossLevel0", false]], "unitsacrosslevel0 (large_image_source_mapnik.girder_source.mapnikgirdertilesource attribute)": [[26, "large_image_source_mapnik.girder_source.MapnikGirderTileSource.unitsAcrossLevel0", false]], "unitsacrosslevel0 (large_image_source_mapnik.mapnikfiletilesource attribute)": [[26, "large_image_source_mapnik.MapnikFileTileSource.unitsAcrossLevel0", false]], "unitsacrosslevel0 (large_image_source_rasterio.girder_source.rasteriogirdertilesource attribute)": [[40, "large_image_source_rasterio.girder_source.RasterioGirderTileSource.unitsAcrossLevel0", false]], "unitsacrosslevel0 (large_image_source_rasterio.rasteriofiletilesource attribute)": [[40, "large_image_source_rasterio.RasterioFileTileSource.unitsAcrossLevel0", false]], "update_frame() (large_image.tilesource.jupyter.map method)": [[11, "large_image.tilesource.jupyter.Map.update_frame", false]], "updateannotation() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.updateAnnotation", false]], "updateannotation() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.updateAnnotation", false]], "updateannotationaccess() (girder_large_image_annotation.rest.annotation.annotationresource method)": [[6, "girder_large_image_annotation.rest.annotation.AnnotationResource.updateAnnotationAccess", false]], "updateelementchunk() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.updateElementChunk", false]], "updateelements() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.updateElements", false]], "updatemetadatakey() (girder_large_image.rest.item_meta.internalmetadataitemresource method)": [[2, "girder_large_image.rest.item_meta.InternalMetadataItemResource.updateMetadataKey", false]], "userschema (girder_large_image_annotation.models.annotation.annotationschema attribute)": [[5, "girder_large_image_annotation.models.annotation.AnnotationSchema.userSchema", false]], "validate() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.validate", false]], "validateboolean() (in module girder_large_image)": [[0, "girder_large_image.validateBoolean", false]], "validateboolean() (in module girder_large_image_annotation)": [[4, "girder_large_image_annotation.validateBoolean", false]], "validatebooleanorall() (in module girder_large_image)": [[0, "girder_large_image.validateBooleanOrAll", false]], "validatebooleanoriccintent() (in module girder_large_image)": [[0, "girder_large_image.validateBooleanOrICCIntent", false]], "validatecog() (large_image_source_gdal.gdalfiletilesource method)": [[24, "large_image_source_gdal.GDALFileTileSource.validateCOG", false]], "validatecog() (large_image_source_rasterio.rasteriofiletilesource method)": [[40, "large_image_source_rasterio.RasterioFileTileSource.validateCOG", false]], "validatedefaultviewer() (in module girder_large_image)": [[0, "girder_large_image.validateDefaultViewer", false]], "validatedictorjson() (in module girder_large_image)": [[0, "girder_large_image.validateDictOrJSON", false]], "validatefolder() (in module girder_large_image)": [[0, "girder_large_image.validateFolder", false]], "validateinfo() (large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.dicomwebassetstoreadapter static method)": [[20, "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter.validateInfo", false]], "validateinfo() (large_image_source_dicom.assetstore.dicomwebassetstoreadapter static method)": [[20, "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter.validateInfo", false]], "validatenonnegativeinteger() (in module girder_large_image)": [[0, "girder_large_image.validateNonnegativeInteger", false]], "validationtifferror": [[44, "large_image_source_tiff.exceptions.ValidationTiffError", false]], "validatorannotation (girder_large_image_annotation.models.annotation.annotation attribute)": [[5, "girder_large_image_annotation.models.annotation.Annotation.validatorAnnotation", false]], "validatorannotationelement (girder_large_image_annotation.models.annotation.annotation attribute)": [[5, "girder_large_image_annotation.models.annotation.Annotation.validatorAnnotationElement", false]], "versionlist() (girder_large_image_annotation.models.annotation.annotation method)": [[5, "girder_large_image_annotation.models.annotation.Annotation.versionList", false]], "vipsfiletilesource (class in large_image_source_vips)": [[48, "large_image_source_vips.VipsFileTileSource", false]], "vipsgirdertilesource (class in large_image_source_vips.girder_source)": [[48, "large_image_source_vips.girder_source.VipsGirderTileSource", false]], "wrapkey() (large_image.tilesource.base.tilesource method)": [[11, "large_image.tilesource.base.TileSource.wrapKey", false]], "wrapkey() (large_image.tilesource.tilesource method)": [[11, "large_image.tilesource.TileSource.wrapKey", false]], "write() (large_image_source_vips.vipsfiletilesource method)": [[48, "large_image_source_vips.VipsFileTileSource.write", false]], "write() (large_image_source_zarr.zarrfiletilesource method)": [[50, "large_image_source_zarr.ZarrFileTileSource.write", false]], "yamlconfigfile() (in module girder_large_image)": [[0, "girder_large_image.yamlConfigFile", false]], "yamlconfigfilewrite() (in module girder_large_image)": [[0, "girder_large_image.yamlConfigFileWrite", false]], "yieldelements() (girder_large_image_annotation.models.annotationelement.annotationelement method)": [[5, "girder_large_image_annotation.models.annotationelement.Annotationelement.yieldElements", false]], "zarrfiletilesource (class in large_image_source_zarr)": [[50, "large_image_source_zarr.ZarrFileTileSource", false]], "zarrgirdertilesource (class in large_image_source_zarr.girder_source)": [[50, "large_image_source_zarr.girder_source.ZarrGirderTileSource", false]]}, "objects": {"": [[0, 0, 0, "-", "girder_large_image"], [4, 0, 0, "-", "girder_large_image_annotation"], [9, 0, 0, "-", "large_image"], [13, 0, 0, "-", "large_image_converter"], [15, 0, 0, "-", "large_image_source_bioformats"], [17, 0, 0, "-", "large_image_source_deepzoom"], [19, 0, 0, "-", "large_image_source_dicom"], [22, 0, 0, "-", "large_image_source_dummy"], [24, 0, 0, "-", "large_image_source_gdal"], [26, 0, 0, "-", "large_image_source_mapnik"], [28, 0, 0, "-", "large_image_source_multi"], [30, 0, 0, "-", "large_image_source_nd2"], [32, 0, 0, "-", "large_image_source_ometiff"], [34, 0, 0, "-", "large_image_source_openjpeg"], [36, 0, 0, "-", "large_image_source_openslide"], [38, 0, 0, "-", "large_image_source_pil"], [40, 0, 0, "-", "large_image_source_rasterio"], [42, 0, 0, "-", "large_image_source_test"], [44, 0, 0, "-", "large_image_source_tiff"], [46, 0, 0, "-", "large_image_source_tifffile"], [48, 0, 0, "-", "large_image_source_vips"], [50, 0, 0, "-", "large_image_source_zarr"], [52, 0, 0, "-", "large_image_tasks"]], "girder_large_image": [[0, 1, 1, "", "LargeImagePlugin"], [0, 4, 1, "", "adjustConfigForUser"], [0, 4, 1, "", "checkForLargeImageFiles"], [0, 0, 0, "-", "constants"], [0, 0, 0, "-", "girder_tilesource"], [0, 4, 1, "", "handleCopyItem"], [0, 4, 1, "", "handleFileSave"], [0, 4, 1, "", "handleRemoveFile"], [0, 4, 1, "", "handleSettingSave"], [0, 0, 0, "-", "loadmodelcache"], [0, 4, 1, "", "metadataSearchHandler"], [1, 0, 0, "-", "models"], [0, 4, 1, "", "patchMount"], [0, 4, 1, "", "prepareCopyItem"], [0, 4, 1, "", "removeThumbnails"], [2, 0, 0, "-", "rest"], [0, 4, 1, "", "unbindGirderEventsByHandlerName"], [0, 4, 1, "", "validateBoolean"], [0, 4, 1, "", "validateBooleanOrAll"], [0, 4, 1, "", "validateBooleanOrICCIntent"], [0, 4, 1, "", "validateDefaultViewer"], [0, 4, 1, "", "validateDictOrJSON"], [0, 4, 1, "", "validateFolder"], [0, 4, 1, "", "validateNonnegativeInteger"], [0, 4, 1, "", "yamlConfigFile"], [0, 4, 1, "", "yamlConfigFileWrite"]], "girder_large_image.LargeImagePlugin": [[0, 2, 1, "", "CLIENT_SOURCE_PATH"], [0, 2, 1, "", "DISPLAY_NAME"], [0, 3, 1, "", "load"]], "girder_large_image.constants": [[0, 1, 1, "", "PluginSettings"]], "girder_large_image.constants.PluginSettings": [[0, 2, 1, "", "LARGE_IMAGE_AUTO_SET"], [0, 2, 1, "", "LARGE_IMAGE_AUTO_USE_ALL_FILES"], [0, 2, 1, "", "LARGE_IMAGE_CONFIG_FOLDER"], [0, 2, 1, "", "LARGE_IMAGE_DEFAULT_VIEWER"], [0, 2, 1, "", "LARGE_IMAGE_ICC_CORRECTION"], [0, 2, 1, "", "LARGE_IMAGE_MAX_SMALL_IMAGE_SIZE"], [0, 2, 1, "", "LARGE_IMAGE_MAX_THUMBNAIL_FILES"], [0, 2, 1, "", "LARGE_IMAGE_NOTIFICATION_STREAM_FALLBACK"], [0, 2, 1, "", "LARGE_IMAGE_SHOW_EXTRA"], [0, 2, 1, "", "LARGE_IMAGE_SHOW_EXTRA_ADMIN"], [0, 2, 1, "", "LARGE_IMAGE_SHOW_EXTRA_PUBLIC"], [0, 2, 1, "", "LARGE_IMAGE_SHOW_ITEM_EXTRA"], [0, 2, 1, "", "LARGE_IMAGE_SHOW_ITEM_EXTRA_ADMIN"], [0, 2, 1, "", "LARGE_IMAGE_SHOW_ITEM_EXTRA_PUBLIC"], [0, 2, 1, "", "LARGE_IMAGE_SHOW_THUMBNAILS"], [0, 2, 1, "", "LARGE_IMAGE_SHOW_VIEWER"]], "girder_large_image.girder_tilesource": [[0, 1, 1, "", "GirderTileSource"], [0, 4, 1, "", "getGirderTileSource"], [0, 4, 1, "", "getGirderTileSourceName"], [0, 4, 1, "", "loadGirderTileSources"]], "girder_large_image.girder_tilesource.GirderTileSource": [[0, 2, 1, "", "extensionsWithAdjacentFiles"], [0, 3, 1, "", "getLRUHash"], [0, 3, 1, "", "getState"], [0, 2, 1, "", "girderSource"], [0, 3, 1, "", "mayHaveAdjacentFiles"], [0, 2, 1, "", "mimeTypesWithAdjacentFiles"]], "girder_large_image.loadmodelcache": [[0, 4, 1, "", "invalidateLoadModelCache"], [0, 4, 1, "", "loadModel"]], "girder_large_image.models": [[1, 0, 0, "-", "image_item"]], "girder_large_image.models.image_item": [[1, 1, 1, "", "ImageItem"]], "girder_large_image.models.image_item.ImageItem": [[1, 3, 1, "", "convertImage"], [1, 3, 1, "", "createImageItem"], [1, 3, 1, "", "delete"], [1, 3, 1, "", "getAndCacheImageOrDataRun"], [1, 3, 1, "", "getAssociatedImage"], [1, 3, 1, "", "getAssociatedImagesList"], [1, 3, 1, "", "getBandInformation"], [1, 3, 1, "", "getInternalMetadata"], [1, 3, 1, "", "getMetadata"], [1, 3, 1, "", "getPixel"], [1, 3, 1, "", "getRegion"], [1, 3, 1, "", "getThumbnail"], [1, 3, 1, "", "getTile"], [1, 3, 1, "", "histogram"], [1, 3, 1, "", "initialize"], [1, 3, 1, "", "removeThumbnailFiles"], [1, 3, 1, "", "tileFrames"], [1, 3, 1, "", "tileSource"]], "girder_large_image.rest": [[2, 4, 1, "", "addSystemEndpoints"], [2, 4, 1, "", "getYAMLConfigFile"], [2, 0, 0, "-", "item_meta"], [2, 0, 0, "-", "large_image_resource"], [2, 4, 1, "", "putYAMLConfigFile"], [2, 0, 0, "-", "tiles"]], "girder_large_image.rest.item_meta": [[2, 1, 1, "", "InternalMetadataItemResource"]], "girder_large_image.rest.item_meta.InternalMetadataItemResource": [[2, 3, 1, "", "deleteMetadataKey"], [2, 3, 1, "", "getMetadataKey"], [2, 3, 1, "", "updateMetadataKey"]], "girder_large_image.rest.large_image_resource": [[2, 1, 1, "", "LargeImageResource"], [2, 4, 1, "", "createThumbnailsJob"], [2, 4, 1, "", "createThumbnailsJobLog"], [2, 4, 1, "", "createThumbnailsJobTask"], [2, 4, 1, "", "createThumbnailsJobThread"], [2, 4, 1, "", "cursorNextOrNone"]], "girder_large_image.rest.large_image_resource.LargeImageResource": [[2, 3, 1, "", "cacheClear"], [2, 3, 1, "", "cacheInfo"], [2, 3, 1, "", "configFormat"], [2, 3, 1, "", "configReplace"], [2, 3, 1, "", "configValidate"], [2, 3, 1, "", "countAssociatedImages"], [2, 3, 1, "", "countHistograms"], [2, 3, 1, "", "countThumbnails"], [2, 3, 1, "", "createLargeImages"], [2, 3, 1, "", "createThumbnails"], [2, 3, 1, "", "deleteAssociatedImages"], [2, 3, 1, "", "deleteHistograms"], [2, 3, 1, "", "deleteIncompleteTiles"], [2, 3, 1, "", "deleteThumbnails"], [2, 3, 1, "", "getPublicSettings"], [2, 3, 1, "", "listSources"]], "girder_large_image.rest.tiles": [[2, 1, 1, "", "TilesItemResource"]], "girder_large_image.rest.tiles.TilesItemResource": [[2, 3, 1, "", "addTilesThumbnails"], [2, 3, 1, "", "convertImage"], [2, 3, 1, "", "createTiles"], [2, 3, 1, "", "deleteTiles"], [2, 3, 1, "", "deleteTilesThumbnails"], [2, 3, 1, "", "getAssociatedImage"], [2, 3, 1, "", "getAssociatedImageMetadata"], [2, 3, 1, "", "getAssociatedImagesList"], [2, 3, 1, "", "getBandInformation"], [2, 3, 1, "", "getDZIInfo"], [2, 3, 1, "", "getDZITile"], [2, 3, 1, "", "getHistogram"], [2, 3, 1, "", "getInternalMetadata"], [2, 3, 1, "", "getTestTile"], [2, 3, 1, "", "getTestTilesInfo"], [2, 3, 1, "", "getTile"], [2, 3, 1, "", "getTileWithFrame"], [2, 3, 1, "", "getTilesInfo"], [2, 3, 1, "", "getTilesPixel"], [2, 3, 1, "", "getTilesRegion"], [2, 3, 1, "", "getTilesThumbnail"], [2, 3, 1, "", "listTilesThumbnails"], [2, 3, 1, "", "tileFrames"], [2, 3, 1, "", "tileFramesQuadInfo"]], "girder_large_image_annotation": [[4, 1, 1, "", "LargeImageAnnotationPlugin"], [4, 0, 0, "-", "constants"], [4, 0, 0, "-", "handlers"], [4, 4, 1, "", "metadataSearchHandler"], [5, 0, 0, "-", "models"], [6, 0, 0, "-", "rest"], [7, 0, 0, "-", "utils"], [4, 4, 1, "", "validateBoolean"]], "girder_large_image_annotation.LargeImageAnnotationPlugin": [[4, 2, 1, "", "CLIENT_SOURCE_PATH"], [4, 2, 1, "", "DISPLAY_NAME"], [4, 3, 1, "", "load"]], "girder_large_image_annotation.handlers": [[4, 4, 1, "", "process_annotations"], [4, 4, 1, "", "resolveAnnotationGirderIds"]], "girder_large_image_annotation.models": [[5, 0, 0, "-", "annotation"], [5, 0, 0, "-", "annotationelement"]], "girder_large_image_annotation.models.annotation": [[5, 1, 1, "", "Annotation"], [5, 1, 1, "", "AnnotationSchema"], [5, 4, 1, "", "extendSchema"]], "girder_large_image_annotation.models.annotation.Annotation": [[5, 1, 1, "", "Skill"], [5, 2, 1, "", "baseFields"], [5, 3, 1, "", "createAnnotation"], [5, 3, 1, "", "deleteMetadata"], [5, 3, 1, "", "findAnnotatedImages"], [5, 3, 1, "", "geojson"], [5, 3, 1, "", "getVersion"], [5, 2, 1, "", "idRegex"], [5, 3, 1, "", "initialize"], [5, 3, 1, "", "injectAnnotationGroupSet"], [5, 3, 1, "", "load"], [5, 2, 1, "", "numberInstance"], [5, 3, 1, "", "remove"], [5, 3, 1, "", "removeOldAnnotations"], [5, 3, 1, "", "revertVersion"], [5, 3, 1, "", "save"], [5, 3, 1, "", "setAccessList"], [5, 3, 1, "", "setMetadata"], [5, 3, 1, "", "updateAnnotation"], [5, 3, 1, "", "validate"], [5, 2, 1, "", "validatorAnnotation"], [5, 2, 1, "", "validatorAnnotationElement"], [5, 3, 1, "", "versionList"]], "girder_large_image_annotation.models.annotation.Annotation.Skill": [[5, 2, 1, "", "EXPERT"], [5, 2, 1, "", "NOVICE"]], "girder_large_image_annotation.models.annotation.AnnotationSchema": [[5, 2, 1, "", "annotationElementSchema"], [5, 2, 1, "", "annotationSchema"], [5, 2, 1, "", "arrowShapeSchema"], [5, 2, 1, "", "baseElementSchema"], [5, 2, 1, "", "baseRectangleShapeSchema"], [5, 2, 1, "", "baseShapeSchema"], [5, 2, 1, "", "circleShapeSchema"], [5, 2, 1, "", "colorRangeSchema"], [5, 2, 1, "", "colorSchema"], [5, 2, 1, "", "coordSchema"], [5, 2, 1, "", "coordValueSchema"], [5, 2, 1, "", "ellipseShapeSchema"], [5, 2, 1, "", "griddataSchema"], [5, 2, 1, "", "groupSchema"], [5, 2, 1, "", "heatmapSchema"], [5, 2, 1, "", "labelSchema"], [5, 2, 1, "", "overlaySchema"], [5, 2, 1, "", "pixelmapCategorySchema"], [5, 2, 1, "", "pixelmapSchema"], [5, 2, 1, "", "pointShapeSchema"], [5, 2, 1, "", "polylineShapeSchema"], [5, 2, 1, "", "rangeValueSchema"], [5, 2, 1, "", "rectangleGridShapeSchema"], [5, 2, 1, "", "rectangleShapeSchema"], [5, 2, 1, "", "transformArray"], [5, 2, 1, "", "userSchema"]], "girder_large_image_annotation.models.annotationelement": [[5, 1, 1, "", "Annotationelement"]], "girder_large_image_annotation.models.annotationelement.Annotationelement": [[5, 2, 1, "", "bboxKeys"], [5, 3, 1, "", "countElements"], [5, 3, 1, "", "getElementGroupSet"], [5, 3, 1, "", "getElements"], [5, 3, 1, "", "getNextVersionValue"], [5, 3, 1, "", "initialize"], [5, 3, 1, "", "removeElements"], [5, 3, 1, "", "removeOldElements"], [5, 3, 1, "", "removeWithQuery"], [5, 3, 1, "", "saveElementAsFile"], [5, 3, 1, "", "updateElementChunk"], [5, 3, 1, "", "updateElements"], [5, 3, 1, "", "yieldElements"]], "girder_large_image_annotation.rest": [[6, 0, 0, "-", "annotation"]], "girder_large_image_annotation.rest.annotation": [[6, 1, 1, "", "AnnotationResource"]], "girder_large_image_annotation.rest.annotation.AnnotationResource": [[6, 3, 1, "", "canCreateFolderAnnotations"], [6, 3, 1, "", "copyAnnotation"], [6, 3, 1, "", "createAnnotation"], [6, 3, 1, "", "createItemAnnotations"], [6, 3, 1, "", "deleteAnnotation"], [6, 3, 1, "", "deleteFolderAnnotations"], [6, 3, 1, "", "deleteItemAnnotations"], [6, 3, 1, "", "deleteMetadata"], [6, 3, 1, "", "deleteOldAnnotations"], [6, 3, 1, "", "existFolderAnnotations"], [6, 3, 1, "", "find"], [6, 3, 1, "", "findAnnotatedImages"], [6, 3, 1, "", "getAnnotation"], [6, 3, 1, "", "getAnnotationAccess"], [6, 3, 1, "", "getAnnotationHistory"], [6, 3, 1, "", "getAnnotationHistoryList"], [6, 3, 1, "", "getAnnotationSchema"], [6, 3, 1, "", "getAnnotationWithFormat"], [6, 3, 1, "", "getFolderAnnotations"], [6, 3, 1, "", "getItemAnnotations"], [6, 3, 1, "", "getItemListAnnotationCounts"], [6, 3, 1, "", "getItemPlottableData"], [6, 3, 1, "", "getItemPlottableElements"], [6, 3, 1, "", "getOldAnnotations"], [6, 3, 1, "", "returnFolderAnnotations"], [6, 3, 1, "", "revertAnnotationHistory"], [6, 3, 1, "", "setFolderAnnotationAccess"], [6, 3, 1, "", "setMetadata"], [6, 3, 1, "", "updateAnnotation"], [6, 3, 1, "", "updateAnnotationAccess"]], "girder_large_image_annotation.utils": [[7, 1, 1, "", "AnnotationGeoJSON"], [7, 1, 1, "", "GeoJSONAnnotation"], [7, 1, 1, "", "PlottableItemData"], [7, 4, 1, "", "isGeoJSON"]], "girder_large_image_annotation.utils.AnnotationGeoJSON": [[7, 3, 1, "", "circleType"], [7, 3, 1, "", "elementToGeoJSON"], [7, 3, 1, "", "ellipseType"], [7, 5, 1, "", "geojson"], [7, 3, 1, "", "pointType"], [7, 3, 1, "", "polylineType"], [7, 3, 1, "", "rectangleType"], [7, 3, 1, "", "rotate"]], "girder_large_image_annotation.utils.GeoJSONAnnotation": [[7, 5, 1, "", "annotation"], [7, 3, 1, "", "annotationToJSON"], [7, 3, 1, "", "circleType"], [7, 5, 1, "", "elementCount"], [7, 5, 1, "", "elements"], [7, 3, 1, "", "ellipseType"], [7, 3, 1, "", "linestringType"], [7, 3, 1, "", "multilinestringType"], [7, 3, 1, "", "multipointType"], [7, 3, 1, "", "multipolygonType"], [7, 3, 1, "", "pointType"], [7, 3, 1, "", "polygonType"], [7, 3, 1, "", "polylineType"], [7, 3, 1, "", "rectangleType"]], "girder_large_image_annotation.utils.PlottableItemData": [[7, 2, 1, "", "allowedTypes"], [7, 5, 1, "", "columns"], [7, 2, 1, "", "commonColumns"], [7, 3, 1, "", "data"], [7, 3, 1, "", "datafileAnnotationElementSelector"], [7, 3, 1, "", "itemNameIDSelector"], [7, 3, 1, "", "keySelector"], [7, 2, 1, "", "maxAnnotationElements"], [7, 2, 1, "", "maxDistinct"], [7, 2, 1, "", "maxItems"], [7, 3, 1, "", "recordSelector"]], "large_image": [[10, 0, 0, "-", "cache_util"], [9, 0, 0, "-", "config"], [9, 0, 0, "-", "constants"], [9, 0, 0, "-", "exceptions"], [11, 0, 0, "-", "tilesource"]], "large_image.cache_util": [[10, 1, 1, "", "CacheFactory"], [10, 1, 1, "", "LruCacheMetaclass"], [10, 1, 1, "", "MemCache"], [10, 1, 1, "", "RedisCache"], [10, 0, 0, "-", "base"], [10, 0, 0, "-", "cache"], [10, 0, 0, "-", "cachefactory"], [10, 4, 1, "", "getTileCache"], [10, 4, 1, "", "isTileCacheSetup"], [10, 0, 0, "-", "memcache"], [10, 4, 1, "", "methodcache"], [10, 4, 1, "", "pickAvailableCache"], [10, 0, 0, "-", "rediscache"], [10, 4, 1, "", "strhash"]], "large_image.cache_util.CacheFactory": [[10, 3, 1, "", "getCache"], [10, 3, 1, "", "getCacheSize"], [10, 2, 1, "", "logged"]], "large_image.cache_util.LruCacheMetaclass": [[10, 2, 1, "", "classCaches"], [10, 2, 1, "", "namedCaches"]], "large_image.cache_util.MemCache": [[10, 3, 1, "", "clear"], [10, 5, 1, "", "curritems"], [10, 5, 1, "", "currsize"], [10, 3, 1, "", "getCache"], [10, 5, 1, "", "maxsize"]], "large_image.cache_util.RedisCache": [[10, 3, 1, "", "clear"], [10, 5, 1, "", "curritems"], [10, 5, 1, "", "currsize"], [10, 3, 1, "", "getCache"], [10, 5, 1, "", "maxsize"]], "large_image.cache_util.base": [[10, 1, 1, "", "BaseCache"]], "large_image.cache_util.base.BaseCache": [[10, 3, 1, "", "clear"], [10, 5, 1, "", "curritems"], [10, 5, 1, "", "currsize"], [10, 3, 1, "", "getCache"], [10, 3, 1, "", "logError"], [10, 5, 1, "", "maxsize"]], "large_image.cache_util.cache": [[10, 1, 1, "", "LruCacheMetaclass"], [10, 4, 1, "", "getTileCache"], [10, 4, 1, "", "isTileCacheSetup"], [10, 4, 1, "", "methodcache"], [10, 4, 1, "", "strhash"]], "large_image.cache_util.cache.LruCacheMetaclass": [[10, 2, 1, "", "classCaches"], [10, 2, 1, "", "namedCaches"]], "large_image.cache_util.cachefactory": [[10, 1, 1, "", "CacheFactory"], [10, 4, 1, "", "getFirstAvailableCache"], [10, 4, 1, "", "loadCaches"], [10, 4, 1, "", "pickAvailableCache"]], "large_image.cache_util.cachefactory.CacheFactory": [[10, 3, 1, "", "getCache"], [10, 3, 1, "", "getCacheSize"], [10, 2, 1, "", "logged"]], "large_image.cache_util.memcache": [[10, 1, 1, "", "MemCache"]], "large_image.cache_util.memcache.MemCache": [[10, 3, 1, "", "clear"], [10, 5, 1, "", "curritems"], [10, 5, 1, "", "currsize"], [10, 3, 1, "", "getCache"], [10, 5, 1, "", "maxsize"]], "large_image.cache_util.rediscache": [[10, 1, 1, "", "RedisCache"]], "large_image.cache_util.rediscache.RedisCache": [[10, 3, 1, "", "clear"], [10, 5, 1, "", "curritems"], [10, 5, 1, "", "currsize"], [10, 3, 1, "", "getCache"], [10, 5, 1, "", "maxsize"]], "large_image.config": [[9, 4, 1, "", "cpu_count"], [9, 4, 1, "", "getConfig"], [9, 4, 1, "", "getLogger"], [9, 4, 1, "", "minimizeCaching"], [9, 4, 1, "", "setConfig"], [9, 4, 1, "", "total_memory"]], "large_image.constants": [[9, 1, 1, "", "SourcePriority"]], "large_image.constants.SourcePriority": [[9, 2, 1, "", "FALLBACK"], [9, 2, 1, "", "FALLBACK_HIGH"], [9, 2, 1, "", "HIGH"], [9, 2, 1, "", "HIGHER"], [9, 2, 1, "", "IMPLICIT"], [9, 2, 1, "", "IMPLICIT_HIGH"], [9, 2, 1, "", "LOW"], [9, 2, 1, "", "LOWER"], [9, 2, 1, "", "MANUAL"], [9, 2, 1, "", "MEDIUM"], [9, 2, 1, "", "NAMED"], [9, 2, 1, "", "PREFERRED"]], "large_image.exceptions": [[9, 6, 1, "", "TileCacheConfigurationError"], [9, 6, 1, "", "TileCacheError"], [9, 6, 1, "", "TileGeneralError"], [9, 2, 1, "", "TileGeneralException"], [9, 6, 1, "", "TileSourceAssetstoreError"], [9, 2, 1, "", "TileSourceAssetstoreException"], [9, 6, 1, "", "TileSourceError"], [9, 2, 1, "", "TileSourceException"], [9, 6, 1, "", "TileSourceFileNotFoundError"], [9, 6, 1, "", "TileSourceInefficientError"], [9, 6, 1, "", "TileSourceXYZRangeError"]], "large_image.tilesource": [[11, 1, 1, "", "FileTileSource"], [11, 6, 1, "", "TileGeneralError"], [11, 2, 1, "", "TileGeneralException"], [11, 1, 1, "", "TileSource"], [11, 6, 1, "", "TileSourceAssetstoreError"], [11, 2, 1, "", "TileSourceAssetstoreException"], [11, 6, 1, "", "TileSourceError"], [11, 2, 1, "", "TileSourceException"], [11, 6, 1, "", "TileSourceFileNotFoundError"], [11, 0, 0, "-", "base"], [11, 4, 1, "", "canRead"], [11, 4, 1, "", "dictToEtree"], [11, 4, 1, "", "etreeToDict"], [11, 0, 0, "-", "geo"], [11, 4, 1, "", "getSourceNameFromDict"], [11, 4, 1, "", "getTileSource"], [11, 0, 0, "-", "jupyter"], [11, 4, 1, "", "listExtensions"], [11, 4, 1, "", "listMimeTypes"], [11, 4, 1, "", "listSources"], [11, 4, 1, "", "nearPowerOfTwo"], [11, 4, 1, "", "new"], [11, 4, 1, "", "open"], [11, 0, 0, "-", "resample"], [11, 0, 0, "-", "stylefuncs"], [11, 0, 0, "-", "tiledict"], [11, 0, 0, "-", "tileiterator"], [11, 0, 0, "-", "utilities"]], "large_image.tilesource.FileTileSource": [[11, 3, 1, "", "canRead"], [11, 3, 1, "", "getLRUHash"], [11, 3, 1, "", "getState"]], "large_image.tilesource.TileSource": [[11, 5, 1, "", "bandCount"], [11, 3, 1, "", "canRead"], [11, 3, 1, "", "convertRegionScale"], [11, 5, 1, "", "dtype"], [11, 2, 1, "", "extensions"], [11, 5, 1, "", "frames"], [11, 5, 1, "", "geospatial"], [11, 3, 1, "", "getAssociatedImage"], [11, 3, 1, "", "getAssociatedImagesList"], [11, 3, 1, "", "getBandInformation"], [11, 3, 1, "", "getBounds"], [11, 3, 1, "", "getCenter"], [11, 3, 1, "", "getICCProfiles"], [11, 3, 1, "", "getInternalMetadata"], [11, 3, 1, "", "getLRUHash"], [11, 3, 1, "", "getLevelForMagnification"], [11, 3, 1, "", "getMagnificationForLevel"], [11, 3, 1, "", "getMetadata"], [11, 3, 1, "", "getNativeMagnification"], [11, 3, 1, "", "getOneBandInformation"], [11, 3, 1, "", "getPixel"], [11, 3, 1, "", "getPointAtAnotherScale"], [11, 3, 1, "", "getPreferredLevel"], [11, 3, 1, "", "getRegion"], [11, 3, 1, "", "getRegionAtAnotherScale"], [11, 3, 1, "", "getSingleTile"], [11, 3, 1, "", "getSingleTileAtAnotherScale"], [11, 3, 1, "", "getState"], [11, 3, 1, "", "getThumbnail"], [11, 3, 1, "", "getTile"], [11, 3, 1, "", "getTileCount"], [11, 3, 1, "", "getTileMimeType"], [11, 3, 1, "", "histogram"], [11, 2, 1, "", "levels"], [11, 5, 1, "", "metadata"], [11, 2, 1, "", "mimeTypes"], [11, 2, 1, "", "name"], [11, 2, 1, "", "nameMatches"], [11, 2, 1, "", "newPriority"], [11, 2, 1, "", "sizeX"], [11, 2, 1, "", "sizeY"], [11, 5, 1, "", "style"], [11, 3, 1, "", "tileFrames"], [11, 2, 1, "", "tileHeight"], [11, 3, 1, "", "tileIterator"], [11, 3, 1, "", "tileIteratorAtAnotherScale"], [11, 2, 1, "", "tileWidth"], [11, 3, 1, "", "wrapKey"]], "large_image.tilesource.base": [[11, 1, 1, "", "FileTileSource"], [11, 1, 1, "", "TileSource"]], "large_image.tilesource.base.FileTileSource": [[11, 3, 1, "", "canRead"], [11, 3, 1, "", "getLRUHash"], [11, 3, 1, "", "getState"]], "large_image.tilesource.base.TileSource": [[11, 5, 1, "", "bandCount"], [11, 3, 1, "", "canRead"], [11, 3, 1, "", "convertRegionScale"], [11, 5, 1, "", "dtype"], [11, 2, 1, "", "extensions"], [11, 5, 1, "", "frames"], [11, 5, 1, "", "geospatial"], [11, 3, 1, "", "getAssociatedImage"], [11, 3, 1, "", "getAssociatedImagesList"], [11, 3, 1, "", "getBandInformation"], [11, 3, 1, "", "getBounds"], [11, 3, 1, "", "getCenter"], [11, 3, 1, "", "getICCProfiles"], [11, 3, 1, "", "getInternalMetadata"], [11, 3, 1, "", "getLRUHash"], [11, 3, 1, "", "getLevelForMagnification"], [11, 3, 1, "", "getMagnificationForLevel"], [11, 3, 1, "", "getMetadata"], [11, 3, 1, "", "getNativeMagnification"], [11, 3, 1, "", "getOneBandInformation"], [11, 3, 1, "", "getPixel"], [11, 3, 1, "", "getPointAtAnotherScale"], [11, 3, 1, "", "getPreferredLevel"], [11, 3, 1, "", "getRegion"], [11, 3, 1, "", "getRegionAtAnotherScale"], [11, 3, 1, "", "getSingleTile"], [11, 3, 1, "", "getSingleTileAtAnotherScale"], [11, 3, 1, "", "getState"], [11, 3, 1, "", "getThumbnail"], [11, 3, 1, "", "getTile"], [11, 3, 1, "", "getTileCount"], [11, 3, 1, "", "getTileMimeType"], [11, 3, 1, "", "histogram"], [11, 5, 1, "", "metadata"], [11, 2, 1, "", "mimeTypes"], [11, 2, 1, "", "name"], [11, 2, 1, "", "nameMatches"], [11, 2, 1, "", "newPriority"], [11, 5, 1, "", "style"], [11, 3, 1, "", "tileFrames"], [11, 3, 1, "", "tileIterator"], [11, 3, 1, "", "tileIteratorAtAnotherScale"], [11, 3, 1, "", "wrapKey"]], "large_image.tilesource.geo": [[11, 1, 1, "", "GDALBaseFileTileSource"], [11, 1, 1, "", "GeoBaseFileTileSource"], [11, 4, 1, "", "make_vsi"]], "large_image.tilesource.geo.GDALBaseFileTileSource": [[11, 2, 1, "", "extensions"], [11, 5, 1, "", "geospatial"], [11, 3, 1, "", "getBounds"], [11, 3, 1, "", "getHexColors"], [11, 3, 1, "", "getNativeMagnification"], [11, 3, 1, "", "getPixelSizeInMeters"], [11, 3, 1, "", "getThumbnail"], [11, 3, 1, "", "getTileCorners"], [11, 3, 1, "", "isGeospatial"], [11, 2, 1, "", "mimeTypes"], [11, 3, 1, "", "pixelToProjection"], [11, 2, 1, "", "projection"], [11, 2, 1, "", "projectionOrigin"], [11, 2, 1, "", "sourceLevels"], [11, 2, 1, "", "sourceSizeX"], [11, 2, 1, "", "sourceSizeY"], [11, 3, 1, "", "toNativePixelCoordinates"], [11, 2, 1, "", "unitsAcrossLevel0"]], "large_image.tilesource.jupyter": [[11, 1, 1, "", "IPyLeafletMixin"], [11, 1, 1, "", "Map"], [11, 4, 1, "", "launch_tile_server"]], "large_image.tilesource.jupyter.IPyLeafletMixin": [[11, 2, 1, "", "JUPYTER_HOST"], [11, 2, 1, "", "JUPYTER_PROXY"], [11, 3, 1, "", "as_leaflet_layer"], [11, 5, 1, "", "iplmap"]], "large_image.tilesource.jupyter.Map": [[11, 3, 1, "", "from_map"], [11, 5, 1, "", "id"], [11, 5, 1, "", "layer"], [11, 3, 1, "", "make_layer"], [11, 3, 1, "", "make_map"], [11, 5, 1, "", "map"], [11, 5, 1, "", "metadata"], [11, 3, 1, "", "to_map"], [11, 3, 1, "", "update_frame"]], "large_image.tilesource.resample": [[11, 1, 1, "", "ResampleMethod"], [11, 4, 1, "", "downsampleTileHalfRes"], [11, 4, 1, "", "numpyResize"], [11, 4, 1, "", "pilResize"]], "large_image.tilesource.resample.ResampleMethod": [[11, 2, 1, "", "NP_MAX"], [11, 2, 1, "", "NP_MAX_COLOR"], [11, 2, 1, "", "NP_MEAN"], [11, 2, 1, "", "NP_MEDIAN"], [11, 2, 1, "", "NP_MIN"], [11, 2, 1, "", "NP_MIN_COLOR"], [11, 2, 1, "", "NP_MODE"], [11, 2, 1, "", "NP_NEAREST"], [11, 2, 1, "", "PIL_BICUBIC"], [11, 2, 1, "", "PIL_BILINEAR"], [11, 2, 1, "", "PIL_BOX"], [11, 2, 1, "", "PIL_HAMMING"], [11, 2, 1, "", "PIL_LANCZOS"], [11, 2, 1, "", "PIL_MAX_ENUM"], [11, 2, 1, "", "PIL_NEAREST"]], "large_image.tilesource.stylefuncs": [[11, 4, 1, "", "maskPixelValues"], [11, 4, 1, "", "medianFilter"]], "large_image.tilesource.tiledict": [[11, 1, 1, "", "LazyTileDict"]], "large_image.tilesource.tiledict.LazyTileDict": [[11, 3, 1, "", "release"], [11, 3, 1, "", "setFormat"]], "large_image.tilesource.tileiterator": [[11, 1, 1, "", "TileIterator"]], "large_image.tilesource.utilities": [[11, 1, 1, "", "ImageBytes"], [11, 1, 1, "", "JSONDict"], [11, 4, 1, "", "addPILFormatsToOutputOptions"], [11, 4, 1, "", "dictToEtree"], [11, 4, 1, "", "etreeToDict"], [11, 4, 1, "", "fullAlphaValue"], [11, 4, 1, "", "getAvailableNamedPalettes"], [11, 4, 1, "", "getPaletteColors"], [11, 4, 1, "", "getTileFramesQuadInfo"], [11, 4, 1, "", "histogramThreshold"], [11, 4, 1, "", "isValidPalette"], [11, 4, 1, "", "nearPowerOfTwo"]], "large_image.tilesource.utilities.ImageBytes": [[11, 5, 1, "", "mimetype"]], "large_image_converter": [[13, 4, 1, "", "convert"], [13, 0, 0, "-", "format_aperio"], [13, 4, 1, "", "format_hook"], [13, 4, 1, "", "is_geospatial"], [13, 4, 1, "", "is_vips"], [13, 4, 1, "", "json_serial"]], "large_image_converter.format_aperio": [[13, 4, 1, "", "adjust_params"], [13, 4, 1, "", "create_thumbnail_and_label"], [13, 4, 1, "", "modify_tiff_before_write"], [13, 4, 1, "", "modify_tiled_ifd"], [13, 4, 1, "", "modify_vips_image_before_output"]], "large_image_source_bioformats": [[15, 1, 1, "", "BioformatsFileTileSource"], [15, 4, 1, "", "canRead"], [15, 0, 0, "-", "girder_source"], [15, 4, 1, "", "open"]], "large_image_source_bioformats.BioformatsFileTileSource": [[15, 3, 1, "", "addKnownExtensions"], [15, 2, 1, "", "cacheName"], [15, 2, 1, "", "extensions"], [15, 3, 1, "", "getAssociatedImagesList"], [15, 3, 1, "", "getInternalMetadata"], [15, 3, 1, "", "getMetadata"], [15, 3, 1, "", "getNativeMagnification"], [15, 3, 1, "", "getTile"], [15, 2, 1, "", "mimeTypes"], [15, 2, 1, "", "name"]], "large_image_source_bioformats.girder_source": [[15, 1, 1, "", "BioformatsGirderTileSource"]], "large_image_source_bioformats.girder_source.BioformatsGirderTileSource": [[15, 2, 1, "", "cacheName"], [15, 2, 1, "", "levels"], [15, 3, 1, "", "mayHaveAdjacentFiles"], [15, 2, 1, "", "name"], [15, 2, 1, "", "sizeX"], [15, 2, 1, "", "sizeY"], [15, 2, 1, "", "tileHeight"], [15, 2, 1, "", "tileWidth"]], "large_image_source_deepzoom": [[17, 1, 1, "", "DeepzoomFileTileSource"], [17, 4, 1, "", "canRead"], [17, 0, 0, "-", "girder_source"], [17, 4, 1, "", "open"]], "large_image_source_deepzoom.DeepzoomFileTileSource": [[17, 2, 1, "", "cacheName"], [17, 2, 1, "", "extensions"], [17, 3, 1, "", "getInternalMetadata"], [17, 3, 1, "", "getTile"], [17, 2, 1, "", "mimeTypes"], [17, 2, 1, "", "name"]], "large_image_source_deepzoom.girder_source": [[17, 1, 1, "", "DeepzoomGirderTileSource"]], "large_image_source_deepzoom.girder_source.DeepzoomGirderTileSource": [[17, 2, 1, "", "cacheName"], [17, 2, 1, "", "levels"], [17, 2, 1, "", "name"], [17, 2, 1, "", "sizeX"], [17, 2, 1, "", "sizeY"], [17, 2, 1, "", "tileHeight"], [17, 2, 1, "", "tileWidth"]], "large_image_source_dicom": [[19, 1, 1, "", "DICOMFileTileSource"], [20, 0, 0, "-", "assetstore"], [19, 4, 1, "", "canRead"], [19, 0, 0, "-", "dicom_metadata"], [19, 0, 0, "-", "dicom_tags"], [19, 4, 1, "", "dicom_to_dict"], [19, 0, 0, "-", "dicomweb_utils"], [19, 0, 0, "-", "girder_plugin"], [19, 0, 0, "-", "girder_source"], [19, 4, 1, "", "open"]], "large_image_source_dicom.DICOMFileTileSource": [[19, 2, 1, "", "cacheName"], [19, 2, 1, "", "extensions"], [19, 3, 1, "", "getAssociatedImagesList"], [19, 3, 1, "", "getInternalMetadata"], [19, 3, 1, "", "getMetadata"], [19, 3, 1, "", "getNativeMagnification"], [19, 3, 1, "", "getTile"], [19, 2, 1, "", "mimeTypes"], [19, 2, 1, "", "name"], [19, 2, 1, "", "nameMatches"]], "large_image_source_dicom.assetstore": [[20, 1, 1, "", "DICOMwebAssetstoreAdapter"], [20, 0, 0, "-", "dicomweb_assetstore_adapter"], [20, 4, 1, "", "load"], [20, 0, 0, "-", "rest"]], "large_image_source_dicom.assetstore.DICOMwebAssetstoreAdapter": [[20, 5, 1, "", "assetstore_meta"], [20, 5, 1, "", "auth_session"], [20, 3, 1, "", "deleteFile"], [20, 3, 1, "", "downloadFile"], [20, 3, 1, "", "finalizeUpload"], [20, 3, 1, "", "getFileSize"], [20, 3, 1, "", "importData"], [20, 3, 1, "", "initUpload"], [20, 3, 1, "", "setContentHeaders"], [20, 3, 1, "", "validateInfo"]], "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter": [[20, 1, 1, "", "DICOMwebAssetstoreAdapter"]], "large_image_source_dicom.assetstore.dicomweb_assetstore_adapter.DICOMwebAssetstoreAdapter": [[20, 5, 1, "", "assetstore_meta"], [20, 5, 1, "", "auth_session"], [20, 3, 1, "", "deleteFile"], [20, 3, 1, "", "downloadFile"], [20, 3, 1, "", "finalizeUpload"], [20, 3, 1, "", "getFileSize"], [20, 3, 1, "", "importData"], [20, 3, 1, "", "initUpload"], [20, 3, 1, "", "setContentHeaders"], [20, 3, 1, "", "validateInfo"]], "large_image_source_dicom.assetstore.rest": [[20, 1, 1, "", "DICOMwebAssetstoreResource"]], "large_image_source_dicom.assetstore.rest.DICOMwebAssetstoreResource": [[20, 3, 1, "", "importData"]], "large_image_source_dicom.dicom_metadata": [[19, 4, 1, "", "extract_dicom_metadata"], [19, 4, 1, "", "extract_specimen_metadata"]], "large_image_source_dicom.dicom_tags": [[19, 4, 1, "", "dicom_key_to_tag"]], "large_image_source_dicom.dicomweb_utils": [[19, 4, 1, "", "get_dicomweb_metadata"], [19, 4, 1, "", "get_first_wsi_volume_metadata"]], "large_image_source_dicom.girder_plugin": [[19, 1, 1, "", "DICOMwebPlugin"]], "large_image_source_dicom.girder_plugin.DICOMwebPlugin": [[19, 2, 1, "", "CLIENT_SOURCE_PATH"], [19, 2, 1, "", "DISPLAY_NAME"], [19, 3, 1, "", "load"]], "large_image_source_dicom.girder_source": [[19, 1, 1, "", "DICOMGirderTileSource"]], "large_image_source_dicom.girder_source.DICOMGirderTileSource": [[19, 2, 1, "", "cacheName"], [19, 2, 1, "", "levels"], [19, 2, 1, "", "name"], [19, 2, 1, "", "sizeX"], [19, 2, 1, "", "sizeY"], [19, 2, 1, "", "tileHeight"], [19, 2, 1, "", "tileWidth"]], "large_image_source_dummy": [[22, 1, 1, "", "DummyTileSource"], [22, 4, 1, "", "canRead"], [22, 4, 1, "", "open"]], "large_image_source_dummy.DummyTileSource": [[22, 3, 1, "", "canRead"], [22, 2, 1, "", "extensions"], [22, 3, 1, "", "getTile"], [22, 2, 1, "", "name"]], "large_image_source_gdal": [[24, 1, 1, "", "GDALFileTileSource"], [24, 4, 1, "", "canRead"], [24, 0, 0, "-", "girder_source"], [24, 4, 1, "", "open"]], "large_image_source_gdal.GDALFileTileSource": [[24, 3, 1, "", "addKnownExtensions"], [24, 2, 1, "", "cacheName"], [24, 5, 1, "", "geospatial"], [24, 3, 1, "", "getBandInformation"], [24, 3, 1, "", "getBounds"], [24, 3, 1, "", "getInternalMetadata"], [24, 3, 1, "", "getLRUHash"], [24, 3, 1, "", "getMetadata"], [24, 3, 1, "", "getPixel"], [24, 3, 1, "", "getProj4String"], [24, 3, 1, "", "getRegion"], [24, 3, 1, "", "getState"], [24, 3, 1, "", "getTile"], [24, 3, 1, "", "isGeospatial"], [24, 2, 1, "", "levels"], [24, 2, 1, "", "name"], [24, 3, 1, "", "pixelToProjection"], [24, 2, 1, "", "projection"], [24, 2, 1, "", "projectionOrigin"], [24, 2, 1, "", "sizeX"], [24, 2, 1, "", "sizeY"], [24, 2, 1, "", "sourceLevels"], [24, 2, 1, "", "sourceSizeX"], [24, 2, 1, "", "sourceSizeY"], [24, 2, 1, "", "tileHeight"], [24, 2, 1, "", "tileWidth"], [24, 3, 1, "", "toNativePixelCoordinates"], [24, 2, 1, "", "unitsAcrossLevel0"], [24, 3, 1, "", "validateCOG"]], "large_image_source_gdal.girder_source": [[24, 1, 1, "", "GDALGirderTileSource"]], "large_image_source_gdal.girder_source.GDALGirderTileSource": [[24, 2, 1, "", "cacheName"], [24, 3, 1, "", "getLRUHash"], [24, 2, 1, "", "levels"], [24, 2, 1, "", "name"], [24, 2, 1, "", "projection"], [24, 2, 1, "", "projectionOrigin"], [24, 2, 1, "", "sizeX"], [24, 2, 1, "", "sizeY"], [24, 2, 1, "", "sourceLevels"], [24, 2, 1, "", "sourceSizeX"], [24, 2, 1, "", "sourceSizeY"], [24, 2, 1, "", "tileHeight"], [24, 2, 1, "", "tileWidth"], [24, 2, 1, "", "unitsAcrossLevel0"]], "large_image_source_mapnik": [[26, 1, 1, "", "MapnikFileTileSource"], [26, 4, 1, "", "canRead"], [26, 0, 0, "-", "girder_source"], [26, 4, 1, "", "open"]], "large_image_source_mapnik.MapnikFileTileSource": [[26, 3, 1, "", "addStyle"], [26, 2, 1, "", "cacheName"], [26, 2, 1, "", "extensions"], [26, 3, 1, "", "getOneBandInformation"], [26, 3, 1, "", "getTile"], [26, 3, 1, "", "interpolateMinMax"], [26, 2, 1, "", "levels"], [26, 2, 1, "", "mimeTypes"], [26, 2, 1, "", "name"], [26, 2, 1, "", "projection"], [26, 2, 1, "", "projectionOrigin"], [26, 2, 1, "", "sizeX"], [26, 2, 1, "", "sizeY"], [26, 2, 1, "", "sourceLevels"], [26, 2, 1, "", "sourceSizeX"], [26, 2, 1, "", "sourceSizeY"], [26, 2, 1, "", "tileHeight"], [26, 2, 1, "", "tileWidth"], [26, 2, 1, "", "unitsAcrossLevel0"]], "large_image_source_mapnik.girder_source": [[26, 1, 1, "", "MapnikGirderTileSource"]], "large_image_source_mapnik.girder_source.MapnikGirderTileSource": [[26, 2, 1, "", "cacheName"], [26, 2, 1, "", "levels"], [26, 2, 1, "", "name"], [26, 2, 1, "", "projection"], [26, 2, 1, "", "projectionOrigin"], [26, 2, 1, "", "sizeX"], [26, 2, 1, "", "sizeY"], [26, 2, 1, "", "sourceLevels"], [26, 2, 1, "", "sourceSizeX"], [26, 2, 1, "", "sourceSizeY"], [26, 2, 1, "", "tileHeight"], [26, 2, 1, "", "tileWidth"], [26, 2, 1, "", "unitsAcrossLevel0"]], "large_image_source_multi": [[28, 1, 1, "", "MultiFileTileSource"], [28, 4, 1, "", "canRead"], [28, 0, 0, "-", "girder_source"], [28, 4, 1, "", "open"]], "large_image_source_multi.MultiFileTileSource": [[28, 2, 1, "", "cacheName"], [28, 2, 1, "", "extensions"], [28, 3, 1, "", "getAssociatedImage"], [28, 3, 1, "", "getAssociatedImagesList"], [28, 3, 1, "", "getInternalMetadata"], [28, 3, 1, "", "getMetadata"], [28, 3, 1, "", "getNativeMagnification"], [28, 3, 1, "", "getTile"], [28, 2, 1, "", "mimeTypes"], [28, 2, 1, "", "name"]], "large_image_source_multi.girder_source": [[28, 1, 1, "", "MultiGirderTileSource"]], "large_image_source_multi.girder_source.MultiGirderTileSource": [[28, 2, 1, "", "cacheName"], [28, 2, 1, "", "levels"], [28, 2, 1, "", "name"], [28, 2, 1, "", "sizeX"], [28, 2, 1, "", "sizeY"], [28, 2, 1, "", "tileHeight"], [28, 2, 1, "", "tileWidth"]], "large_image_source_nd2": [[30, 1, 1, "", "ND2FileTileSource"], [30, 4, 1, "", "canRead"], [30, 4, 1, "", "diffObj"], [30, 0, 0, "-", "girder_source"], [30, 4, 1, "", "namedtupleToDict"], [30, 4, 1, "", "open"]], "large_image_source_nd2.ND2FileTileSource": [[30, 2, 1, "", "cacheName"], [30, 2, 1, "", "extensions"], [30, 3, 1, "", "getInternalMetadata"], [30, 3, 1, "", "getMetadata"], [30, 3, 1, "", "getNativeMagnification"], [30, 3, 1, "", "getTile"], [30, 2, 1, "", "mimeTypes"], [30, 2, 1, "", "name"]], "large_image_source_nd2.girder_source": [[30, 1, 1, "", "ND2GirderTileSource"]], "large_image_source_nd2.girder_source.ND2GirderTileSource": [[30, 2, 1, "", "cacheName"], [30, 2, 1, "", "levels"], [30, 2, 1, "", "name"], [30, 2, 1, "", "sizeX"], [30, 2, 1, "", "sizeY"], [30, 2, 1, "", "tileHeight"], [30, 2, 1, "", "tileWidth"]], "large_image_source_ometiff": [[32, 1, 1, "", "OMETiffFileTileSource"], [32, 4, 1, "", "canRead"], [32, 0, 0, "-", "girder_source"], [32, 4, 1, "", "open"]], "large_image_source_ometiff.OMETiffFileTileSource": [[32, 2, 1, "", "cacheName"], [32, 2, 1, "", "extensions"], [32, 3, 1, "", "getInternalMetadata"], [32, 3, 1, "", "getMetadata"], [32, 3, 1, "", "getNativeMagnification"], [32, 3, 1, "", "getPreferredLevel"], [32, 3, 1, "", "getTile"], [32, 2, 1, "", "mimeTypes"], [32, 2, 1, "", "name"]], "large_image_source_ometiff.girder_source": [[32, 1, 1, "", "OMETiffGirderTileSource"]], "large_image_source_ometiff.girder_source.OMETiffGirderTileSource": [[32, 2, 1, "", "cacheName"], [32, 2, 1, "", "levels"], [32, 2, 1, "", "name"], [32, 2, 1, "", "sizeX"], [32, 2, 1, "", "sizeY"], [32, 2, 1, "", "tileHeight"], [32, 2, 1, "", "tileWidth"]], "large_image_source_openjpeg": [[34, 1, 1, "", "OpenjpegFileTileSource"], [34, 4, 1, "", "canRead"], [34, 0, 0, "-", "girder_source"], [34, 4, 1, "", "open"]], "large_image_source_openjpeg.OpenjpegFileTileSource": [[34, 2, 1, "", "cacheName"], [34, 2, 1, "", "extensions"], [34, 3, 1, "", "getAssociatedImagesList"], [34, 3, 1, "", "getInternalMetadata"], [34, 3, 1, "", "getNativeMagnification"], [34, 3, 1, "", "getTile"], [34, 2, 1, "", "mimeTypes"], [34, 2, 1, "", "name"]], "large_image_source_openjpeg.girder_source": [[34, 1, 1, "", "OpenjpegGirderTileSource"]], "large_image_source_openjpeg.girder_source.OpenjpegGirderTileSource": [[34, 2, 1, "", "cacheName"], [34, 2, 1, "", "levels"], [34, 3, 1, "", "mayHaveAdjacentFiles"], [34, 2, 1, "", "name"], [34, 2, 1, "", "sizeX"], [34, 2, 1, "", "sizeY"], [34, 2, 1, "", "tileHeight"], [34, 2, 1, "", "tileWidth"]], "large_image_source_openslide": [[36, 1, 1, "", "OpenslideFileTileSource"], [36, 4, 1, "", "canRead"], [36, 0, 0, "-", "girder_source"], [36, 4, 1, "", "open"]], "large_image_source_openslide.OpenslideFileTileSource": [[36, 2, 1, "", "cacheName"], [36, 2, 1, "", "extensions"], [36, 3, 1, "", "getAssociatedImagesList"], [36, 3, 1, "", "getInternalMetadata"], [36, 3, 1, "", "getNativeMagnification"], [36, 3, 1, "", "getPreferredLevel"], [36, 3, 1, "", "getTile"], [36, 2, 1, "", "mimeTypes"], [36, 2, 1, "", "name"]], "large_image_source_openslide.girder_source": [[36, 1, 1, "", "OpenslideGirderTileSource"]], "large_image_source_openslide.girder_source.OpenslideGirderTileSource": [[36, 2, 1, "", "cacheName"], [36, 2, 1, "", "extensionsWithAdjacentFiles"], [36, 2, 1, "", "levels"], [36, 2, 1, "", "mimeTypesWithAdjacentFiles"], [36, 2, 1, "", "name"], [36, 2, 1, "", "sizeX"], [36, 2, 1, "", "sizeY"], [36, 2, 1, "", "tileHeight"], [36, 2, 1, "", "tileWidth"]], "large_image_source_pil": [[38, 1, 1, "", "PILFileTileSource"], [38, 4, 1, "", "canRead"], [38, 4, 1, "", "getMaxSize"], [38, 0, 0, "-", "girder_source"], [38, 4, 1, "", "open"]], "large_image_source_pil.PILFileTileSource": [[38, 3, 1, "", "addKnownExtensions"], [38, 2, 1, "", "cacheName"], [38, 3, 1, "", "defaultMaxSize"], [38, 2, 1, "", "extensions"], [38, 3, 1, "", "getInternalMetadata"], [38, 3, 1, "", "getLRUHash"], [38, 3, 1, "", "getMetadata"], [38, 3, 1, "", "getState"], [38, 3, 1, "", "getTile"], [38, 2, 1, "", "mimeTypes"], [38, 2, 1, "", "name"]], "large_image_source_pil.girder_source": [[38, 1, 1, "", "PILGirderTileSource"]], "large_image_source_pil.girder_source.PILGirderTileSource": [[38, 2, 1, "", "cacheName"], [38, 3, 1, "", "defaultMaxSize"], [38, 3, 1, "", "getLRUHash"], [38, 3, 1, "", "getState"], [38, 3, 1, "", "getTile"], [38, 2, 1, "", "levels"], [38, 2, 1, "", "name"], [38, 2, 1, "", "sizeX"], [38, 2, 1, "", "sizeY"], [38, 2, 1, "", "tileHeight"], [38, 2, 1, "", "tileWidth"]], "large_image_source_rasterio": [[40, 1, 1, "", "RasterioFileTileSource"], [40, 4, 1, "", "canRead"], [40, 0, 0, "-", "girder_source"], [40, 4, 1, "", "make_crs"], [40, 4, 1, "", "open"]], "large_image_source_rasterio.RasterioFileTileSource": [[40, 3, 1, "", "addKnownExtensions"], [40, 2, 1, "", "cacheName"], [40, 3, 1, "", "getBandInformation"], [40, 3, 1, "", "getBounds"], [40, 3, 1, "", "getCrs"], [40, 3, 1, "", "getInternalMetadata"], [40, 3, 1, "", "getLRUHash"], [40, 3, 1, "", "getMetadata"], [40, 3, 1, "", "getPixel"], [40, 3, 1, "", "getRegion"], [40, 3, 1, "", "getState"], [40, 3, 1, "", "getTile"], [40, 3, 1, "", "isGeospatial"], [40, 2, 1, "", "levels"], [40, 2, 1, "", "name"], [40, 3, 1, "", "pixelToProjection"], [40, 2, 1, "", "projection"], [40, 2, 1, "", "projectionOrigin"], [40, 2, 1, "", "sizeX"], [40, 2, 1, "", "sizeY"], [40, 2, 1, "", "sourceLevels"], [40, 2, 1, "", "sourceSizeX"], [40, 2, 1, "", "sourceSizeY"], [40, 2, 1, "", "tileHeight"], [40, 2, 1, "", "tileWidth"], [40, 3, 1, "", "toNativePixelCoordinates"], [40, 2, 1, "", "unitsAcrossLevel0"], [40, 3, 1, "", "validateCOG"]], "large_image_source_rasterio.girder_source": [[40, 1, 1, "", "RasterioGirderTileSource"]], "large_image_source_rasterio.girder_source.RasterioGirderTileSource": [[40, 2, 1, "", "cacheName"], [40, 3, 1, "", "getLRUHash"], [40, 2, 1, "", "levels"], [40, 2, 1, "", "name"], [40, 2, 1, "", "projection"], [40, 2, 1, "", "projectionOrigin"], [40, 2, 1, "", "sizeX"], [40, 2, 1, "", "sizeY"], [40, 2, 1, "", "sourceLevels"], [40, 2, 1, "", "sourceSizeX"], [40, 2, 1, "", "sourceSizeY"], [40, 2, 1, "", "tileHeight"], [40, 2, 1, "", "tileWidth"], [40, 2, 1, "", "unitsAcrossLevel0"]], "large_image_source_test": [[42, 1, 1, "", "TestTileSource"], [42, 4, 1, "", "canRead"], [42, 4, 1, "", "open"]], "large_image_source_test.TestTileSource": [[42, 2, 1, "", "cacheName"], [42, 3, 1, "", "canRead"], [42, 2, 1, "", "extensions"], [42, 3, 1, "", "fractalTile"], [42, 3, 1, "", "getInternalMetadata"], [42, 3, 1, "", "getLRUHash"], [42, 3, 1, "", "getMetadata"], [42, 3, 1, "", "getState"], [42, 3, 1, "", "getTile"], [42, 2, 1, "", "levels"], [42, 2, 1, "", "name"], [42, 2, 1, "", "sizeX"], [42, 2, 1, "", "sizeY"], [42, 2, 1, "", "tileHeight"], [42, 2, 1, "", "tileWidth"]], "large_image_source_tiff": [[44, 1, 1, "", "TiffFileTileSource"], [44, 4, 1, "", "canRead"], [44, 0, 0, "-", "exceptions"], [44, 0, 0, "-", "girder_source"], [44, 4, 1, "", "open"], [44, 0, 0, "-", "tiff_reader"]], "large_image_source_tiff.TiffFileTileSource": [[44, 2, 1, "", "cacheName"], [44, 2, 1, "", "extensions"], [44, 3, 1, "", "getAssociatedImagesList"], [44, 3, 1, "", "getInternalMetadata"], [44, 3, 1, "", "getMetadata"], [44, 3, 1, "", "getNativeMagnification"], [44, 3, 1, "", "getTiffDir"], [44, 3, 1, "", "getTile"], [44, 3, 1, "", "getTileIOTiffError"], [44, 2, 1, "", "mimeTypes"], [44, 2, 1, "", "name"]], "large_image_source_tiff.exceptions": [[44, 6, 1, "", "IOOpenTiffError"], [44, 6, 1, "", "IOTiffError"], [44, 6, 1, "", "InvalidOperationTiffError"], [44, 6, 1, "", "TiffError"], [44, 6, 1, "", "ValidationTiffError"]], "large_image_source_tiff.girder_source": [[44, 1, 1, "", "TiffGirderTileSource"]], "large_image_source_tiff.girder_source.TiffGirderTileSource": [[44, 2, 1, "", "cacheName"], [44, 2, 1, "", "levels"], [44, 2, 1, "", "name"], [44, 2, 1, "", "sizeX"], [44, 2, 1, "", "sizeY"], [44, 2, 1, "", "tileHeight"], [44, 2, 1, "", "tileWidth"]], "large_image_source_tiff.tiff_reader": [[44, 1, 1, "", "TiledTiffDirectory"], [44, 4, 1, "", "patchLibtiff"]], "large_image_source_tiff.tiff_reader.TiledTiffDirectory": [[44, 2, 1, "", "CoreFunctions"], [44, 3, 1, "", "getTile"], [44, 5, 1, "", "imageHeight"], [44, 5, 1, "", "imageWidth"], [44, 3, 1, "", "parse_image_description"], [44, 5, 1, "", "pixelInfo"], [44, 5, 1, "", "tileHeight"], [44, 5, 1, "", "tileWidth"]], "large_image_source_tifffile": [[46, 1, 1, "", "TifffileFileTileSource"], [46, 4, 1, "", "canRead"], [46, 1, 1, "", "checkForMissingDataHandler"], [46, 4, 1, "", "et_findall"], [46, 0, 0, "-", "girder_source"], [46, 4, 1, "", "open"]], "large_image_source_tifffile.TifffileFileTileSource": [[46, 3, 1, "", "addKnownExtensions"], [46, 2, 1, "", "cacheName"], [46, 2, 1, "", "extensions"], [46, 3, 1, "", "getAssociatedImagesList"], [46, 3, 1, "", "getInternalMetadata"], [46, 3, 1, "", "getMetadata"], [46, 3, 1, "", "getNativeMagnification"], [46, 3, 1, "", "getTile"], [46, 2, 1, "", "mimeTypes"], [46, 2, 1, "", "name"]], "large_image_source_tifffile.checkForMissingDataHandler": [[46, 3, 1, "", "emit"]], "large_image_source_tifffile.girder_source": [[46, 1, 1, "", "TifffileGirderTileSource"]], "large_image_source_tifffile.girder_source.TifffileGirderTileSource": [[46, 2, 1, "", "cacheName"], [46, 2, 1, "", "levels"], [46, 2, 1, "", "name"], [46, 2, 1, "", "sizeX"], [46, 2, 1, "", "sizeY"], [46, 2, 1, "", "tileHeight"], [46, 2, 1, "", "tileWidth"]], "large_image_source_vips": [[48, 1, 1, "", "VipsFileTileSource"], [48, 4, 1, "", "canRead"], [48, 0, 0, "-", "girder_source"], [48, 4, 1, "", "new"], [48, 4, 1, "", "open"]], "large_image_source_vips.VipsFileTileSource": [[48, 3, 1, "", "addKnownExtensions"], [48, 3, 1, "", "addTile"], [48, 5, 1, "", "bandFormat"], [48, 5, 1, "", "bandRanges"], [48, 2, 1, "", "cacheName"], [48, 5, 1, "", "crop"], [48, 2, 1, "", "extensions"], [48, 3, 1, "", "getInternalMetadata"], [48, 3, 1, "", "getMetadata"], [48, 3, 1, "", "getNativeMagnification"], [48, 3, 1, "", "getState"], [48, 3, 1, "", "getTile"], [48, 2, 1, "", "mimeTypes"], [48, 5, 1, "", "minHeight"], [48, 5, 1, "", "minWidth"], [48, 5, 1, "", "mm_x"], [48, 5, 1, "", "mm_y"], [48, 2, 1, "", "name"], [48, 2, 1, "", "newPriority"], [48, 5, 1, "", "origin"], [48, 3, 1, "", "write"]], "large_image_source_vips.girder_source": [[48, 1, 1, "", "VipsGirderTileSource"]], "large_image_source_vips.girder_source.VipsGirderTileSource": [[48, 2, 1, "", "cacheName"], [48, 2, 1, "", "levels"], [48, 2, 1, "", "name"], [48, 2, 1, "", "sizeX"], [48, 2, 1, "", "sizeY"], [48, 2, 1, "", "tileHeight"], [48, 2, 1, "", "tileWidth"]], "large_image_source_zarr": [[50, 1, 1, "", "ZarrFileTileSource"], [50, 4, 1, "", "canRead"], [50, 0, 0, "-", "girder_source"], [50, 4, 1, "", "new"], [50, 4, 1, "", "open"]], "large_image_source_zarr.ZarrFileTileSource": [[50, 3, 1, "", "addAssociatedImage"], [50, 3, 1, "", "addTile"], [50, 2, 1, "", "cacheName"], [50, 5, 1, "", "channelColors"], [50, 5, 1, "", "channelNames"], [50, 5, 1, "", "crop"], [50, 2, 1, "", "extensions"], [50, 3, 1, "", "getAssociatedImagesList"], [50, 3, 1, "", "getInternalMetadata"], [50, 3, 1, "", "getMetadata"], [50, 3, 1, "", "getNativeMagnification"], [50, 3, 1, "", "getState"], [50, 3, 1, "", "getTile"], [50, 5, 1, "", "imageDescription"], [50, 2, 1, "", "mimeTypes"], [50, 5, 1, "", "mm_x"], [50, 5, 1, "", "mm_y"], [50, 2, 1, "", "name"], [50, 2, 1, "", "newPriority"], [50, 3, 1, "", "write"]], "large_image_source_zarr.girder_source": [[50, 1, 1, "", "ZarrGirderTileSource"]], "large_image_source_zarr.girder_source.ZarrGirderTileSource": [[50, 2, 1, "", "cacheName"], [50, 2, 1, "", "levels"], [50, 2, 1, "", "name"], [50, 2, 1, "", "sizeX"], [50, 2, 1, "", "sizeY"], [50, 2, 1, "", "tileHeight"], [50, 2, 1, "", "tileWidth"]], "large_image_tasks": [[52, 1, 1, "", "LargeImageTasks"], [52, 0, 0, "-", "tasks"]], "large_image_tasks.LargeImageTasks": [[52, 3, 1, "", "task_imports"]], "large_image_tasks.tasks": [[52, 1, 1, "", "JobLogger"], [52, 4, 1, "", "cache_histograms_job"], [52, 4, 1, "", "cache_tile_frames_job"], [52, 4, 1, "", "convert_image_job"]], "large_image_tasks.tasks.JobLogger": [[52, 3, 1, "", "emit"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "attribute", "Python attribute"], "3": ["py", "method", "Python method"], "4": ["py", "function", "Python function"], "5": ["py", "property", "Python property"], "6": ["py", "exception", "Python exception"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:attribute", "3": "py:method", "4": "py:function", "5": "py:property", "6": "py:exception"}, "terms": {"": [0, 4, 5, 11, 19, 20, 24, 40, 54, 57, 58, 61, 62, 64, 66, 67, 68, 70, 72], "0": [0, 1, 2, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 57, 61, 64, 66, 68, 70, 71, 72, 73], "00": [70, 71], "000": [11, 22, 73], "000000": [54, 73], "0000ff": [54, 64, 73], "00025": [61, 71], "0002521": 70, "0004991": 70, "00123": 61, "0025": 61, "00898008224": 70, "00f": [61, 73], "00ff00": [54, 64, 73], "00ffff": [64, 73], "01": [61, 70], "012": 11, "0123": 11, "01234": 11, "01234567": 11, "0123456789abcdef01234567": 54, "01z": 70, "02": [11, 73], "04": 71, "052231141926995": 70, "053": 54, "054": 54, "067": 54, "0f0": [61, 73], "0m": 70, "0th": 11, "0xbbggrr": 11, "1": [5, 9, 10, 11, 13, 22, 24, 26, 32, 36, 40, 48, 50, 54, 57, 61, 64, 66, 68, 70, 71, 72, 73], "10": [9, 11, 13, 54, 64, 70, 71], "100": [13, 28, 61, 64, 70, 71, 73], "1000": [7, 57, 61], "10000": 70, "1000th": 57, "102": 73, "1024": [57, 61], "103m": 70, "1073741824": 11, "109434": 70, "109568": 54, "10976": 54, "11": [9, 11, 70, 71], "11000": 61, "11264": 71, "11520": 61, "11776": 61, "11875961711466": 70, "119": 73, "11a": 70, "12": [11, 58, 70, 71], "122": 70, "12288": 61, "123": 54, "12345": 11, "125": 61, "127": [10, 11, 57], "128": [54, 61], "13": [11, 70], "130081300813009": 61, "13594198": 70, "136": 73, "13660993": 70, "136883384": 70, "1381": 70, "14": [58, 70], "144": 54, "15": [54, 70], "1500": 61, "153": 73, "16": [11, 57, 61, 66, 70], "160": 64, "16256": 70, "16384": 57, "164": 54, "164648651261": 70, "16th": 57, "17": [54, 70, 73], "170": 73, "180": [24, 26, 40, 68], "18000": 64, "187": 73, "192": 54, "1920": 61, "1b75a4ec911017aef5c885760a3c6575dacf5f8efb59fb0e011108dce85b1f4e97b8d358f3363c1f5ea6f1c3698f037554aec1620bbdd4cac54e3d5c9c1da1fd": 70, "1sc": 60, "2": [5, 9, 11, 22, 42, 48, 50, 54, 57, 61, 64, 68, 70, 71, 73], "20": [7, 19, 54, 70], "2000": 67, "2016": 61, "204": 73, "2048": [61, 71], "20d689c6": 70, "2106": 61, "22": [13, 66], "221": 73, "23232": 54, "234": 70, "23456": 11, "235": 70, "236": 70, "237": 70, "238": [70, 73], "239": 70, "24": [5, 54], "240": 70, "241": [54, 70], "242": 70, "243": 70, "24475a89acc0": 70, "25": [54, 61, 64], "250": 64, "251": 70, "253": 70, "255": [11, 22, 24, 40, 42, 54, 68, 70, 71, 73], "256": [11, 61, 66, 70, 71], "262": 54, "27017": 58, "29": 70, "2952k": 71, "297712617": 70, "2d": [5, 54], "2fl": 60, "2i": 54, "2x2": 54, "3": [5, 9, 11, 13, 19, 48, 50, 54, 57, 58, 61, 64, 67, 68, 70, 71, 73], "30": [5, 54], "3000": 71, "31": 54, "311": 54, "32": [54, 57, 70], "32320": 54, "332": 54, "34": [54, 73], "343": 60, "34567": 11, "35": 70, "360": 68, "364": 54, "37": 70, "375": 61, "38": 70, "3840": 61, "3857": [24, 26, 61, 70], "39": [70, 71], "3m": 71, "4": [5, 9, 11, 48, 54, 61, 64, 67, 68, 70, 71, 72], "40": [54, 61, 70, 71], "402": 54, "40864": 54, "4096": [38, 57, 67], "414141": 73, "4194304": 70, "42368": 54, "43": 60, "43000": 64, "431": 54, "4323": 70, "4326": [24, 26, 40], "43811085": 70, "45": [54, 70], "4502326": 70, "45219874192699": 70, "4567": 11, "4586806": 70, "4694": 70, "47": 70, "470217162239": 70, "480": 61, "48416": 54, "498": 54, "5": [5, 9, 11, 54, 61, 64, 68, 70, 71, 72, 73], "50": 71, "500": 61, "5000": [7, 70, 71], "501": 54, "505628098154": 70, "508": 54, "51": 73, "512": [54, 61, 71], "53": 54, "531": 54, "532493975171": 70, "53472": 54, "535": 54, "53760": 61, "555": 54, "55680": 61, "55988": 70, "56": [54, 70], "567": 11, "56832": 61, "57344": 61, "57600": 61, "57856": 61, "57b345d28d777f126827dc28": 70, "5818e9418d777f10f26ee443": 70, "58368": 61, "58b480ba92ca9a000b08c899": 71, "59": 70, "590676043792": 70, "5bbdeec6e629140048d01bb9": 70, "5d5d5d": 73, "5e56cdb8fb1a02615698a153862c10d5292b1ad42836a6e8bce5627e93a387dc0d3c9b6cfbd539796500bc2d3e23eafd07550f8c214e9348880bbbc6b3b0ea0c": 70, "6": [5, 9, 11, 54, 64, 68, 70, 71, 73], "61": 70, "63": 54, "63392": 54, "637": 54, "6379": [10, 57], "640": 61, "647": 54, "65248": 54, "65535": [11, 22, 73], "658": 54, "661": 54, "668": 54, "68": 73, "7": [5, 9, 11, 54, 64, 68, 70, 71], "71879201711467": 70, "720": 68, "727272": 73, "75": 61, "768": 61, "76873": 70, "8": [5, 9, 10, 11, 54, 57, 61, 66, 68, 70, 71, 73], "806": 54, "8192": 57, "838383": 73, "85": [11, 73], "86": 72, "866": 54, "87": 54, "876143450579": 70, "8m": 70, "9": [9, 11, 13, 61, 66, 70, 71, 72], "90": [13, 66], "90504": 70, "92": 72, "9216": 71, "93376": 54, "939393": 73, "95": 11, "951318035": 70, "95758": 70, "96": 70, "96096": 54, "9a": [5, 54], "9m": 70, "A": [0, 5, 11, 22, 24, 40, 44, 57, 59, 61, 62, 64, 66, 67, 73], "As": [11, 54, 57, 61, 67], "At": [54, 61], "But": 61, "By": [5, 58, 60, 61, 64, 67], "For": [2, 5, 7, 11, 13, 24, 26, 40, 48, 54, 57, 58, 61, 64, 66, 67, 68, 70, 71, 72, 73], "If": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 57, 58, 60, 61, 62, 64, 66, 67, 68, 70, 72, 73, 74], "In": [11, 24, 26, 40, 48, 54, 57, 61, 64, 67, 70, 72], "It": [5, 11, 20, 22, 26, 48, 50, 54, 59, 61, 62, 64, 68, 73], "NOT": [5, 11], "No": 7, "Not": 54, "OR": 1, "One": [11, 54, 61], "Or": [58, 68, 69], "That": 57, "The": [0, 1, 2, 4, 5, 7, 10, 11, 13, 19, 20, 22, 24, 26, 40, 42, 44, 48, 50, 54, 55, 56, 57, 58, 59, 60, 61, 62, 64, 66, 67, 68, 70, 71, 73, 74], "There": [56, 57, 61, 62, 64, 67, 70, 72], "These": [1, 5, 11, 54, 56, 57, 58, 61, 62, 64, 67, 69], "To": [11, 58, 61, 64, 65, 67, 69, 73], "_": 72, "__all__": [7, 64], "__del__": 56, "__inherit__": 64, "__init__": [11, 22, 42], "__name__": 57, "__none__": 68, "_bbox": 5, "_concurr": [13, 66], "_core": 0, "_dtypedict": 11, "_encodeimag": 11, "_except_": 63, "_exclude_associ": 66, "_id": [5, 70, 72], "_ipython_display_": 11, "_itemfromev": 4, "_keep_associ": 66, "_supportsdtyp": 11, "_tilecach": 10, "_version": 5, "_vt": 10, "a02o": 70, "a0sp": 70, "a1": 70, "a1a1a1": 73, "a594": 61, "aa": 70, "abl": [11, 44], "about": [2, 11, 15, 17, 19, 20, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 60, 61, 71, 73], "abov": [11, 13, 22, 24, 40, 54, 64, 67, 73], "absolut": [11, 67], "abstract": 11, "abstractassetstoreadapt": 20, "accept": 61, "access": [0, 5, 11, 15, 17, 19, 24, 26, 28, 30, 32, 34, 36, 38, 40, 44, 46, 48, 50, 57, 59, 61, 62, 64, 67, 70, 72, 74], "accesscontrolledmodel": 5, "accommod": [48, 50], "accord": 20, "ace2": 60, "acff": 60, "across": [1, 5, 11, 42, 54, 56], "act": [5, 48, 50, 67], "activ": [58, 63], "actual": [1, 4, 5, 11, 20, 32, 36, 46, 52, 54, 61, 68], "ad": [5, 11, 13, 54, 56, 57, 59, 61, 64, 65, 68, 71, 73], "adapt": 20, "adapt_rgb": 71, "add": [0, 2, 4, 5, 10, 11, 13, 26, 48, 50, 58, 61, 62, 64, 66, 67, 68, 71, 73], "add_lay": 70, "add_tile_to_sourc": 61, "addassociatedimag": [50, 51, 71], "addit": [1, 5, 11, 13, 15, 17, 19, 20, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 61, 62, 67, 68, 70, 73], "addition": [54, 66], "additionalproperti": [5, 54, 68], "addknownextens": [15, 16, 24, 25, 38, 39, 40, 41, 46, 47, 48, 49], "addpilformatstooutputopt": [9, 11], "addstyl": [26, 27], "addsystemendpoint": [0, 2], "addtil": [48, 49, 50, 51, 61, 71], "addtilesthumbnail": [0, 2], "adjac": [7, 11, 24, 40], "adjacentitem": [6, 7, 72], "adjust": [0, 11, 13, 22, 57, 60, 61, 73], "adjust_param": [13, 14], "adjustconfigforus": [0, 3], "admin": [0, 5, 59, 62, 64], "adob": 60, "advis": [5, 54], "aeaeae": 73, "aet": 59, "affect": [10, 60, 62, 64, 72, 73], "affin": [5, 54, 68], "afi": 60, "afm": 60, "after": [11, 20, 22, 48, 50, 59, 68, 72, 73], "afterward": 20, "ag": [5, 6], "again": 11, "aggreg": 54, "aim": 60, "al3d": 60, "algorithm": [61, 71, 73], "ali": 60, "alia": [9, 11], "alias": 11, "align": 11, "all": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 20, 22, 24, 26, 36, 40, 42, 44, 46, 48, 50, 57, 58, 61, 63, 64, 66, 67, 68, 70, 72, 73], "all_sources_ignored_nam": 57, "alloc": 61, "allocate_lock": 10, "allow": [0, 5, 9, 11, 20, 24, 38, 40, 48, 50, 54, 56, 57, 58, 61, 62, 67, 70], "allowcooki": 0, "allowedtyp": [4, 7], "allownul": [5, 6], "along": [11, 54, 57, 68, 72], "alpha": [5, 11, 22, 24, 40, 48, 50, 54, 61, 73], "alphabet": 0, "alreadi": [1, 2, 11, 13, 24, 40], "also": [1, 2, 5, 7, 11, 20, 22, 24, 26, 28, 40, 54, 56, 57, 58, 59, 61, 64, 67, 70, 71, 73], "alter": [20, 48, 50, 64], "altern": [11, 22, 57, 61], "altitud": 61, "alwai": [5, 10, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 57, 61, 62, 64, 66, 73], "am": 60, "ambigu": 11, "amiramesh": 60, "amount": [11, 61, 73], "an": [0, 1, 2, 4, 5, 7, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 57, 58, 59, 60, 62, 63, 64, 65, 66, 67, 68, 70, 71, 72, 73], "analysi": 67, "analyt": 67, "ang": 11, "ani": [1, 5, 7, 9, 10, 11, 15, 17, 19, 20, 22, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 61, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73], "annot": [4, 7, 8, 57, 61, 65, 67, 72], "annotaiton": 7, "annotatioid": 7, "annotationel": [4, 7, 8], "annotationelementschema": [4, 5], "annotationgeojson": [4, 7], "annotationid": [5, 7], "annotationlist": 62, "annotationnam": 54, "annotationresourc": [4, 6], "annotationschema": [4, 5], "annotationtojson": [4, 7], "anoth": [5, 11, 56, 57, 64, 70], "anymap": 60, "anyof": [5, 54], "anyth": 54, "aperio": [13, 66], "api": [2, 11, 62, 63, 64, 67, 70, 71, 72], "apiroot": 2, "apiurl": 70, "apl": 60, "apng": 60, "app": 52, "appear": [7, 64, 66, 68], "appli": [0, 5, 11, 22, 48, 50, 54, 56, 57, 64, 67, 68], "applic": [0, 11, 19, 28, 50, 60, 64, 65], "appropri": [0, 2, 5, 11, 13, 54, 58, 59, 68, 70, 73], "approxim": [11, 61], "ar": [0, 1, 5, 7, 9, 11, 13, 20, 22, 24, 26, 28, 30, 38, 40, 42, 48, 54, 55, 56, 57, 58, 60, 61, 62, 63, 64, 66, 67, 68, 70, 71, 72, 73, 74], "arbitrari": [0, 1, 4, 10, 11, 19, 24, 40, 61, 71, 73], "arc": 59, "archiv": 67, "area": [0, 4, 5, 11, 19, 48, 50, 70, 73], "aren": 58, "arf": 60, "arg": [0, 1, 4, 5, 9, 10, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52], "argument": [1, 10, 11, 24, 28, 40, 66], "aroow": 54, "around": [5, 54], "arr": 11, "arrai": [2, 5, 7, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 61, 64, 67, 68, 70, 71, 72, 73], "arrang": [68, 72], "arrow": 5, "arrowshapeschema": [4, 5], "artifact": [11, 58], "as_leaflet_lay": [9, 11], "asarrai": 44, "asc": 60, "ascii": 68, "asfeatur": 7, "ask": [57, 70, 71], "aspect": [1, 11, 24, 40, 64], "assetstor": [19, 21, 64, 65], "assetstore_meta": [19, 20], "assign": 68, "associ": [1, 5, 7, 11, 13, 15, 17, 19, 28, 34, 36, 38, 44, 46, 50, 54, 64, 66, 71, 72, 73], "assum": [5, 11, 54, 68, 72], "atial": 5, "attach": [5, 11, 20, 26], "attempt": [61, 72], "attribut": [5, 7, 11, 54, 62, 72], "audit": 62, "augment": 20, "auth_sess": [19, 20], "authent": [0, 5, 7, 11, 70], "auto": [11, 22, 64, 73], "auto_set": 0, "auto_use_all_fil": 0, "automat": [11, 48, 64], "autorang": 64, "av": 60, "avail": [0, 5, 9, 10, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 57, 59, 61, 66, 67, 70, 72, 73], "availablecach": 10, "availablegidertilesourc": 0, "availablesourc": 11, "averag": [11, 70, 71], "avi": 60, "avif": 60, "avoid": [11, 68], "ax": [11, 24, 40, 50, 54, 68], "axi": [5, 11, 22, 24, 40, 42, 50, 54, 61, 64, 73], "b": [5, 11, 54, 60, 72], "bababa": 73, "back": [10, 11, 24, 40, 57], "backbon": 67, "backend": [56, 57], "background": 68, "backgroundcolor": 68, "badli": 5, "bag": 60, "balanc": 67, "band": [1, 11, 13, 22, 24, 26, 40, 42, 48, 64, 67, 68, 70, 71, 73], "bandcount": [9, 11, 70, 71], "bandformat": [48, 49], "bandrang": [48, 49], "base": [0, 1, 2, 4, 5, 6, 7, 9, 12, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 57, 60, 61, 62, 64, 65, 66, 67, 68, 73], "base_": 68, "base_pixel": 11, "basecach": [9, 10], "baseelementschema": [4, 5], "basefield": [4, 5], "basepath": 68, "baserectangleshapeschema": [4, 5], "baseshapeschema": [4, 5], "basic": [1, 11, 46, 52, 54, 69, 73], "bbox": [5, 7], "bboxkei": [4, 5], "be76": 70, "becaus": [5, 11, 20, 56, 66, 70], "been": [0, 5, 10, 11, 20, 68, 73], "befor": [1, 5, 11, 20, 61, 68, 73], "begin": [5, 64], "behavior": 20, "being": [20, 44, 63, 73], "belong": 5, "below": [54, 60, 64], "benefici": 61, "benefit": [11, 22], "besid": [58, 73], "better": [11, 57, 64], "between": [2, 5, 11, 22, 24, 26, 40, 54, 56, 71, 73], "beyond": [5, 11, 54], "bicub": 11, "bif": [36, 60], "big": 61, "bigger": 56, "bil": 60, "bilinear": 11, "bin": [11, 58, 60, 73], "bin_edg": 11, "binari": [11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 70], "bioformat": [15, 57, 67], "bioformatsfiletilesourc": [15, 16], "bioformatsgirdertilesourc": [15, 16], "bip": 60, "bit": [11, 61, 66, 67, 73], "black": [11, 61, 70, 73], "blend": 73, "blp": 60, "blue": [11, 22, 42, 61, 70, 73], "blx": 60, "bmp": 60, "bool": [7, 9, 10, 11, 20, 24, 44, 57], "boolean": [5, 11, 13, 44, 54, 68, 72, 73], "border": 70, "both": [1, 11, 24, 38, 40, 42, 44, 61, 64, 65, 66, 67], "bottom": [1, 5, 11, 61, 68, 70, 73], "bound": [5, 7, 11, 24, 40, 61, 70, 71, 72], "boundari": [5, 9, 11, 54, 73], "box": [5, 7, 11, 64, 72], "break": 68, "brightest": [61, 73], "brows": [64, 67], "browser": [57, 62], "bs1": 70, "bt": 60, "btf": 60, "buffer": [11, 44], "bufr": 60, "build": [0, 4, 19, 58, 65], "built": [59, 67], "button": [59, 64], "bw": 60, "byn": 60, "bypass": 68, "byte": [9, 11, 20, 24, 26, 40, 44, 57, 61], "c": [0, 5, 11, 42, 61, 66, 67, 68, 70, 71], "c01": 60, "c5c5c5": 73, "cach": [0, 1, 9, 11, 12, 22, 24, 38, 40, 42, 48, 50, 57, 61, 65, 67], "cache_backend": 57, "cache_histograms_job": [52, 53], "cache_lock": 10, "cache_memcached_password": 57, "cache_memcached_url": 57, "cache_memcached_usernam": 57, "cache_python_memory_port": 57, "cache_redis_password": 57, "cache_redis_url": 57, "cache_redis_usernam": 57, "cache_sourc": 57, "cache_tile_frames_job": [52, 53], "cache_tilesource_maximum": 57, "cache_tilesource_memory_port": 57, "cache_util": [9, 12], "cacheclear": [0, 2], "cachefactori": [9, 12], "cacheinfo": [0, 2], "cachenam": [10, 15, 16, 17, 18, 19, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51], "cachetool": 10, "cal": 60, "calcul": [11, 64], "call": [0, 11, 13, 20, 22, 54, 61, 63, 70, 71, 73], "callabl": 10, "caller": 20, "can": [0, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 57, 58, 59, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73], "cancel": [2, 5], "canceljob": 2, "cancreatefolderannot": [4, 6], "cannot": [7, 11, 22, 42, 44, 48, 66], "canon": [7, 72], "canread": [9, 11, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51], "cap": 11, "capabl": [70, 71], "carri": 70, "case": [5, 11, 20, 24, 26, 48, 54, 61, 64], "cast": [11, 22, 72, 73], "catalog": [11, 22], "categor": 54, "categori": [5, 24, 40, 54, 62, 64], "caus": [5, 11, 44], "caution": 64, "cd": 58, "ceil": [11, 61], "center": [5, 11, 54], "centroid": 5, "certain": 0, "cfg": [57, 60], "cgroup": 9, "ch5": 60, "chang": [0, 11, 20, 24, 40, 62, 66, 74], "channel": [1, 5, 11, 22, 24, 40, 42, 48, 50, 54, 64, 66, 68, 71], "channelcolor": [50, 51], "channelmap": [11, 24, 40, 71], "channelnam": [50, 51], "charact": 54, "charg": 20, "check": [0, 2, 7, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 59, 64, 68, 70], "check_til": 24, "checkandcr": 1, "checkforlargeimagefil": [0, 3], "checkformissingdatahandl": [46, 47], "cherrypi": [0, 20], "child": 46, "choos": [11, 61], "choropleth": [5, 54], "chosen": 48, "chroma": [11, 22], "chromin": 73, "chunk": [5, 20], "chunksiz": 5, "ci": 70, "cif": 60, "circl": 5, "circleshapeschema": [4, 5], "circletyp": [4, 7], "circular": 72, "clamp": [11, 22, 73], "class": [0, 1, 2, 4, 5, 6, 7, 9, 10, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 70, 71, 72], "class_a": 54, "class_b": 54, "class_c": 54, "classcach": [9, 10], "classic": 11, "classmethod": [11, 15, 22, 24, 38, 40, 42, 46, 48], "clear": [0, 9, 10], "client": [0, 4, 11, 19, 58, 59, 70, 72], "client_source_path": [0, 3, 4, 8, 19, 21], "clip": [11, 22, 73], "clone": 58, "close": [5, 54], "cloud": [11, 24, 40, 66, 67], "cmsprofil": 11, "code": [0, 4, 19, 55, 57, 58, 68], "cog": 66, "cogeo": 40, "cogtiff": 40, "col": 7, "colab": [69, 70, 71], "collect": [1, 5, 11, 48, 50, 64, 70], "color": [1, 5, 11, 22, 42, 56, 57, 64, 67, 68, 71], "colorizer_xxx": 26, "colormap": 67, "colorrang": [5, 54], "colorrangeschema": [4, 5], "colorschema": [4, 5], "colort": [24, 40], "column": [4, 7, 44, 62, 64, 72], "com": [58, 59, 61, 65, 70, 71], "combin": [0, 11, 62, 64], "comma": [7, 42, 57], "command": [61, 67, 74], "comment": 54, "common": [58, 59, 61, 67, 70], "commoncolumn": [4, 7], "commonli": 61, "compact": 5, "compar": [30, 71], "compat": [13, 40, 42, 48, 57, 66], "compil": 5, "complet": [44, 58, 64], "complex": [58, 67], "compliant": 67, "compon": [0, 4, 19, 58, 61], "compos": [1, 11, 57, 68], "composit": [1, 11, 22, 26, 28, 61, 64, 67], "compositeop": 26, "compress": [11, 13, 22, 44, 56, 66, 67, 73], "comput": [11, 20, 24, 40, 42, 57, 61, 66, 67, 71, 73], "conceptu": [11, 61, 68], "concurr": [2, 61, 66], "condit": 11, "config": [0, 2, 12, 38, 57, 64], "config_fold": 0, "configformat": [0, 2], "configreplac": [0, 2], "configur": [10, 11, 65, 67, 70], "configvalid": [0, 2], "conform": [66, 68], "conisd": 11, "conserv": 57, "consid": 61, "consist": [48, 50, 54, 61, 64, 67, 73], "consol": [57, 59, 62, 64], "constant": [3, 8, 12, 60, 61, 73], "constrain": 73, "contain": [0, 2, 5, 7, 9, 11, 15, 19, 20, 22, 24, 26, 28, 30, 32, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 64, 72, 73], "content": [3, 8, 12, 14, 16, 18, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 62, 64, 70], "contentdisposit": 20, "context": [0, 11, 70, 73], "continu": 54, "contour": [5, 54], "control": [2, 5, 11, 57, 62, 64], "conveni": [58, 61, 70], "convent": 71, "convers": [11, 13, 67], "convert": [7, 11, 13, 14, 19, 22, 24, 26, 30, 40, 48, 61, 64, 66, 67, 70, 71, 73], "convert_image_job": [52, 53], "converterparam": 50, "convertimag": [0, 1, 2], "convertparam": 13, "convertregionscal": [9, 11], "cooki": 0, "coordin": [5, 11, 24, 40, 61, 70, 72], "coordinatewithvalu": [5, 54], "coordschema": [4, 5], "coordvalueschema": [4, 5], "copi": [0, 71], "copyannot": [4, 6], "core": [58, 60, 67, 72, 73], "corefunct": [44, 45], "corner": [11, 24, 40, 48, 50, 61], "correct": [5, 56, 57], "correspond": [5, 54, 58, 61, 64, 67, 68], "could": [10, 57, 61, 64, 66, 68, 70, 73], "count": [5, 6, 7, 9, 11, 13, 26, 42, 72], "countassociatedimag": [0, 2], "countel": [4, 5], "counterclockwis": [5, 54], "counthistogram": [0, 2], "countthumbnail": [0, 2], "cpu": [2, 9, 13], "cpu_count": [9, 11, 12], "cr": [13, 40, 66], "cr2": 60, "crash": 11, "creat": [0, 1, 2, 5, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 58, 59, 62, 64, 67, 71], "create_thumbnail_and_label": [13, 14], "createannot": [4, 5, 6], "createimageitem": [0, 1], "createitemannot": [4, 6], "createjob": [1, 2], "createlargeimag": [0, 2], "createthumbnail": [0, 2], "createthumbnailsjob": [0, 2], "createthumbnailsjoblog": [0, 2], "createthumbnailsjobtask": [0, 2], "createthumbnailsjobthread": [0, 2], "createtil": [0, 2], "creator": [5, 62], "creatorid": 5, "criteria": 11, "critic": 57, "crop": [11, 22, 48, 49, 50, 51, 66, 68, 71, 73], "croptoimag": 11, "cross": [5, 54, 58], "crowd": 70, "crw": 60, "cset": 68, "css": [54, 73], "csv": 60, "ct1": 60, "cub": 60, "cur": 60, "curl": [70, 71], "current": [0, 7, 10, 11, 22, 24, 40, 48, 50, 54, 64, 70, 71, 73], "curritem": [9, 10], "currsiz": [9, 10], "cursor": 2, "cursornextornon": [0, 2], "custom": [11, 56, 64, 67, 73], "cvalu": 68, "cx": 7, "cxd": 60, "cy": 7, "cy5": 61, "cycl": 11, "czi": [15, 60], "d": [5, 10, 11, 19, 48, 50, 54, 58, 68], "d0d0d0": 73, "dadada": 73, "dai": 5, "dapi": 61, "dat": 60, "data": [1, 2, 4, 5, 7, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 59, 61, 64, 65, 66, 67, 68, 70], "databas": [5, 7, 20, 57, 62, 67, 74], "datafil": 7, "datafileannotationelementselector": [4, 7], "dataset": [19, 24, 40], "datatyp": 64, "date": 62, "datetim": 13, "datum": 70, "db": [50, 60, 71, 74], "dcm": [19, 36, 60], "dcm4chee": 59, "dcm_": 19, "dcx": 60, "dd": 60, "ddf": 60, "decod": 57, "decor": 10, "decreas": 66, "decript": [5, 54, 68], "dedic": 67, "deepzoom": [17, 67], "deepzoomfiletilesourc": [17, 18], "deepzoomgirdertilesourc": [17, 18], "def": [61, 71], "default": [1, 5, 9, 11, 13, 20, 22, 24, 26, 38, 40, 42, 48, 50, 54, 56, 57, 58, 60, 61, 62, 66, 67, 68, 70, 73], "default_view": 0, "defaultmaxs": [38, 39], "defaultsort": [62, 64], "defin": [11, 20, 54, 57, 71, 73], "definit": [11, 64, 73], "deflat": [11, 13, 66], "delet": [0, 1, 5, 20, 64], "delete_on": 5, "deleteannot": [4, 6], "deleteassociatedimag": [0, 2], "deletefil": [19, 20], "deletefolderannot": [4, 6], "deletehistogram": [0, 2], "deleteincompletetil": [0, 2], "deleteitemannot": [4, 6], "deletemetadata": [4, 5, 6], "deletemetadatakei": [0, 2], "deleteoldannot": [4, 6], "deleteresult": 5, "deletethumbnail": [0, 2], "deletetil": [0, 2], "deletetilesthumbnail": [0, 2], "delimit": 7, "dem": 60, "demo": [61, 65, 70, 71], "demonstr": 69, "densiti": 11, "depend": [11, 13, 42, 56, 57, 58, 61, 67, 70, 73], "deploy": 57, "depth": [61, 66], "describ": 11, "descript": [5, 7, 13, 54, 57, 64, 68, 71], "design": 67, "desir": [0, 11, 24, 32, 36, 40, 44, 68, 73], "dest": 66, "destin": [48, 50], "destinationid": 20, "destinationtyp": 20, "detail": [5, 11, 54, 57, 60, 64, 66], "detect": 64, "determin": [1, 9, 56, 57, 73], "dev": 58, "develop": [0, 4, 19, 65, 67], "devenv": 58, "dialog": 64, "dib": 60, "dic": [19, 60], "dicom": [19, 59, 60, 67], "dicom_key_to_tag": [19, 21], "dicom_metadata": 21, "dicom_tag": 21, "dicom_to_dict": [19, 21], "dicomfiletilesourc": [19, 21], "dicomgirdertilesourc": [19, 21], "dicomread": 19, "dicomweb": [19, 20, 65], "dicomweb_assetstore_adapt": [19, 21], "dicomweb_cli": 20, "dicomweb_util": 21, "dicomwebassetstoreadapt": [19, 20], "dicomwebassetstoreresourc": [19, 20], "dicomwebplugin": [19, 21], "dict": [0, 5, 10, 11, 15, 17, 19, 20, 22, 26, 28, 30, 32, 34, 36, 38, 42, 44, 46, 48, 50, 61], "dictionari": [0, 1, 2, 5, 7, 9, 10, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 64, 70, 72, 73], "dicttoetre": [9, 11], "differ": [11, 30, 56, 57, 60, 61, 64, 68, 70, 71, 73], "diffobj": [30, 31], "digit": [54, 61, 67], "dimens": [11, 13, 48, 50, 57, 61, 67, 70], "dimension": [54, 61, 67], "dimmest": 73, "dir": [62, 64], "direct": [5, 54, 57, 61], "directli": [20, 57, 58, 66, 67, 70], "directori": [13, 44, 58, 68], "directorynum": 44, "discret": [26, 54, 73], "disk": [44, 66], "displai": [0, 4, 5, 11, 19, 54, 64, 66, 72], "display_nam": [0, 3, 4, 8, 19, 21], "disposit": 20, "distanc": [11, 24, 26, 40, 64], "distinct": [7, 54, 61, 64, 72], "distinctcount": [7, 72], "distribut": 64, "divid": [11, 22, 24, 26, 40, 68, 73], "dload": [70, 71], "dm2": 60, "dm3": 60, "dm4": 60, "do": [1, 5, 11, 20, 24, 40, 46, 52, 61, 66, 67, 68, 69, 70, 74], "doc": [0, 4, 5, 20, 58], "docker": [11, 58], "doctyp": 7, "document": [0, 5, 7, 20, 58, 67, 70, 73], "doe": [1, 5, 11, 20, 24, 26, 40, 61, 64, 67, 68], "doesn": [1, 5, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 70], "dollar": 5, "domain": [11, 54], "don": [1, 5, 11, 24, 40, 44, 54, 57, 70], "done": [5, 13, 56, 58, 61, 68], "dot": [11, 62, 64], "doubl": [5, 54, 57], "down": [54, 62, 64, 68, 70], "download": [20, 58, 61, 69, 70], "downloadfil": [19, 20], "downsampl": [11, 68], "downsampletilehalfr": [9, 11], "draft6": 68, "draft6valid": 5, "draw": [42, 70], "drawn": 72, "dropdown": 64, "dt0": 60, "dt1": 60, "dt2": 60, "dti": 60, "dtype": [9, 11, 22, 24, 40, 42, 68, 70, 71, 73], "due": [44, 57, 64], "dummi": [22, 67], "dummytilesourc": [22, 23], "dure": [11, 13], "dv": 60, "dwg": 60, "dx": [5, 54], "dx1": 70, "dy": [5, 54], "dynam": [11, 22, 57, 61, 67, 69], "dz": 60, "dzc": [17, 60], "dzi": [17, 60], "e": [0, 5, 11, 24, 40, 58, 64, 67, 68, 73], "e4e4e4": 73, "each": [1, 2, 5, 7, 10, 11, 13, 22, 24, 40, 42, 54, 56, 57, 60, 61, 62, 64, 67, 68, 71, 72, 73], "earlier": 5, "easi": [67, 70], "easier": [57, 61, 67], "eded": 73, "edg": [11, 22, 61], "eer": 60, "efa5": 70, "effect": [11, 61, 64], "effici": [11, 22, 66, 67, 73], "either": [0, 1, 7, 9, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 57, 61, 62, 64, 66, 72, 73], "elem": 7, "element": [4, 5, 7, 10, 11, 46, 67, 72], "elementcount": [4, 7], "elementtogeojson": [4, 7], "elementtre": 11, "elib": [11, 22], "ellips": 5, "ellipseshapeschema": [4, 5], "ellipsetyp": [4, 7], "ellipsoid": 11, "els": [64, 70, 71], "emf": 60, "emit": [46, 47, 48, 50, 52, 53, 67], "empti": [0, 5, 46, 52, 54, 64, 71], "enabl": 70, "encod": [1, 11, 22, 24, 26, 28, 40, 61, 63, 70], "end": [11, 20, 22, 46, 57, 60, 64, 67, 73], "endbyt": 20, "endpoint": [2, 11, 20, 54, 63, 72], "enforc": [24, 40], "enough": [11, 61, 63, 64], "ensur": [5, 9, 13, 58, 73], "enter": 5, "entir": [5, 11, 54, 61, 73], "entri": [1, 2, 5, 7, 10, 11, 19, 24, 40, 54, 62, 64, 73], "entrypoint": [0, 4, 10, 19], "entrypointnam": 10, "enum": [5, 11, 48, 50, 54, 57, 64, 73], "enumer": 71, "env": 58, "environ": [11, 59, 66, 67, 69, 70, 73], "eosin": 64, "ep": 60, "epsg": [24, 26, 40, 61, 70], "epsi": 60, "equal": 5, "equival": [24, 26, 66, 68], "er": 60, "err": [10, 60], "error": [10, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50], "especi": 61, "essenti": 61, "estim": [10, 56, 57], "et": [15, 60], "et_findal": [46, 47], "etc": [11, 13, 61, 66, 71], "etre": 11, "etreetodict": [9, 11], "even": [5, 7, 11, 54, 66, 73], "evenli": 54, "event": [0, 4, 5, 11], "eventu": 5, "everi": [0, 11, 64, 70], "everyth": [61, 67], "exact": [11, 26, 54, 73], "exactli": [11, 73], "examin": 72, "exampl": [11, 54, 56, 57, 61, 63, 64, 65, 67, 71, 72], "exce": [5, 11], "except": [5, 7, 10, 11, 12, 13, 20, 22, 24, 40, 45, 48, 50, 54, 61, 73], "exclud": [11, 61, 64, 66, 68, 73], "exclus": [11, 64, 66, 71], "exclusivemaximum": 64, "exclusiveminimum": [5, 54, 64, 68], "execut": 1, "exist": [1, 2, 5, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 59, 64, 66], "existfolderannot": [4, 6], "exit": 66, "exp": 60, "expand": [48, 50], "expect": [5, 10, 54, 57, 61], "experiment": 57, "expert": 5, "explan": [5, 54], "explicitli": [57, 68], "expos": [11, 24, 40, 73], "express": [57, 64, 68, 72], "exr": 60, "extend": [11, 73], "extendschema": [4, 5], "extens": [0, 9, 11, 15, 16, 17, 18, 19, 21, 22, 23, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 57], "extensionswithadjacentfil": [0, 3, 36, 37], "extent": 26, "extern": [57, 67], "extra": [19, 66], "extract": [5, 11, 22], "extract_dicom_metadata": [19, 21], "extract_specimen_metadata": [19, 21], "extraparamet": 20, "extras_requir": 67, "extrem": 67, "f": [5, 54, 70, 71], "f00": [61, 73], "f6f6f6": 73, "fa": [5, 54], "factor": 11, "fail": [2, 5, 20], "failur": 44, "fairli": 19, "fall": [57, 63], "fallback": [9, 11, 12, 13, 15, 17, 19, 26, 28, 30, 34, 36, 44, 46, 48, 50], "fallback_high": [9, 12, 38], "fals": [0, 1, 2, 5, 6, 7, 9, 10, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 61, 68, 70, 73], "falsi": 73, "fancy_algorithm": 61, "faster": [5, 57, 61], "fdf": 60, "featur": [5, 7, 70], "featurecollect": 7, "fetch": [5, 11, 70], "few": [70, 72], "fewer": [11, 64, 68], "ff0000": [54, 64, 73], "ff00ff": [64, 73], "ff8000": 64, "fff": 60, "ffff00": [64, 73], "ffffff": 73, "ffr": 60, "fgb": 60, "field": [1, 5, 6, 11], "file": [0, 1, 5, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 57, 60, 61, 63, 66, 67, 68, 73], "file10": 68, "file9": 68, "fileid": 0, "filenam": 20, "filenotfounderror": [9, 11], "fileobj": 1, "filepath": 44, "filesystem": [11, 17, 19, 24, 26, 28, 30, 32, 34, 36, 40, 44, 46, 48, 50], "filetilesourc": [0, 9, 11, 15, 17, 19, 28, 30, 34, 36, 38, 42, 44, 46, 48, 50], "fill": [1, 5, 11, 22, 24, 40, 54, 68], "fillcolor": [5, 54], "filter": [5, 11, 20, 46, 52, 59, 62, 64, 71], "final": [11, 20, 68], "finalizeupload": [19, 20], "find": [4, 5, 6, 46, 58, 61, 67, 70, 71], "find_spec": [70, 71], "findannotatedimag": [4, 5, 6], "fine": 64, "finish": 0, "fire": 5, "first": [0, 1, 5, 7, 11, 13, 22, 28, 30, 54, 57, 61, 64, 66, 68, 70, 73], "fit": [10, 60, 61], "fix": [10, 11, 61], "fize": 54, "flag": [5, 11, 20, 54, 72], "flat": 19, "flc": 60, "fledg": 11, "flex": 60, "fli": 60, "float": [5, 7, 10, 11, 13, 22, 24, 26, 40, 42, 54, 61, 64, 66, 72, 73], "float64": 71, "fluoresc": 61, "fly": 61, "focal": 61, "folder": [0, 2, 6, 7, 17, 64, 67, 69, 72], "follow": [0, 5, 11, 13, 20, 22, 26, 54, 55, 61, 64, 67, 68, 69, 72, 73], "fontsiz": [5, 54], "footprint": 71, "footprint_s": 71, "forc": 5, "forg": [61, 67], "form": [5, 11, 22, 24, 26, 42, 54, 73], "format": [6, 11, 13, 15, 17, 19, 22, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 57, 61, 62, 64, 66, 67, 70, 71], "format_aperio": 14, "format_check": 5, "format_hook": [13, 14], "formatorregionmim": [11, 24, 40], "formatt": [46, 52], "found": [64, 72, 73], "four": [11, 24, 40, 64], "fractal": [42, 67], "fractaltil": [42, 43], "fraction": [10, 11, 56], "frame": [1, 2, 9, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 66, 67, 71], "framebas": 11, "framedelta": [11, 22, 64], "framegroup": 11, "framegroupfactor": 11, "framegroupstrid": 11, "framelist": [1, 11], "framesacross": [1, 11], "framesasax": 68, "framestrid": 11, "framevalu": 68, "free": [54, 65], "frequent": 61, "friendli": 72, "frm": 60, "from": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 55, 58, 59, 61, 62, 63, 64, 65, 67, 68, 70, 71, 72, 73], "from_map": [9, 11], "frommax": 11, "fromstr": 11, "front": 2, "ft": 60, "ftc": 60, "ftu": 60, "full": [7, 11, 13, 22, 42, 58, 61, 66, 73], "full_check": 24, "fullalphavalu": [9, 11], "fulli": 11, "func": 10, "funcnam": 13, "function": [0, 1, 2, 5, 7, 10, 11, 13, 20, 24, 38, 40, 42, 48, 50, 57, 61, 69, 70, 71, 73], "further": 13, "g": [0, 11, 24, 40, 54, 58, 67, 68, 73], "gather": 72, "gaussian": 54, "gb": 9, "gbr": 60, "gc": [11, 70], "gc1": 70, "gc2": 70, "gdal": [11, 24, 61, 67], "gdalbasefiletilesourc": [9, 11, 24, 40], "gdalfiletilesourc": [24, 25, 26], "gdalgirdertilesourc": [24, 25, 26], "gdb": 60, "gear": [62, 64], "gel": 60, "gen": 60, "gener": [0, 1, 5, 7, 10, 20, 42, 54, 55, 57, 60, 63, 66, 67, 70, 72, 73], "geo": [9, 12, 61, 67], "geobasefiletilesourc": [9, 11], "geodata": 67, "geoj": 64, "geojson": [4, 5, 7], "geojsonannot": [4, 7], "geom": 7, "geospati": [9, 11, 13, 24, 25, 26, 40, 61, 66, 67, 73], "geot": 70, "geotiff": [11, 24, 26, 40, 60, 66, 67], "get": [0, 1, 2, 5, 7, 9, 10, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 58, 63, 64, 67, 70, 71], "get_dicomweb_metadata": [19, 21], "get_first_wsi_volume_metadata": [19, 21], "getandcacheimageordatarun": [0, 1], "getannot": [4, 6], "getannotationaccess": [4, 6], "getannotationhistori": [4, 6], "getannotationhistorylist": [4, 6], "getannotationschema": [4, 6], "getannotationwithformat": [4, 6], "getassociatedimag": [0, 1, 2, 9, 11, 28, 29, 61], "getassociatedimagemetadata": [0, 2], "getassociatedimageslist": [0, 1, 2, 9, 11, 15, 16, 19, 21, 28, 29, 34, 35, 36, 37, 44, 45, 46, 47, 50, 51, 61], "getavailablenamedpalett": [9, 11], "getbandinform": [0, 1, 2, 9, 11, 24, 25, 26, 40, 41], "getbound": [9, 11, 24, 25, 40, 41, 61], "getcach": [9, 10], "getcaches": [9, 10], "getcent": [9, 11], "getcolor": 11, "getconfig": [9, 12, 57], "getcr": [40, 41], "getdziinfo": [0, 2], "getdzitil": [0, 2], "getel": [4, 5], "getelementgroupset": [4, 5], "getfield": 44, "getfiles": [19, 20], "getfirstavailablecach": [9, 10], "getfolderannot": [4, 6], "getgirdertilesourc": [0, 3], "getgirdertilesourcenam": [0, 3], "gethexcolor": [9, 11], "gethistogram": [0, 2], "geticcprofil": [9, 11], "getinternalmetadata": [0, 1, 2, 9, 11, 15, 16, 17, 18, 19, 21, 24, 25, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51], "getitemannot": [4, 6], "getitemlistannotationcount": [4, 6], "getitemplottabledata": [4, 6], "getitemplottableel": [4, 6], "getlevelformagnif": [9, 11], "getlogg": [9, 12, 57], "getlruhash": [0, 3, 9, 11, 24, 25, 38, 39, 40, 41, 42, 43], "getmagnificationforlevel": [9, 11], "getmaxs": [38, 39], "getmetadata": [0, 1, 9, 11, 15, 16, 19, 21, 24, 25, 28, 29, 30, 31, 32, 33, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 61, 71], "getmetadatakei": [0, 2], "getmod": 44, "getnativemagnif": [9, 11, 15, 16, 19, 21, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 44, 45, 46, 47, 48, 49, 50, 51], "getnextversionvalu": [4, 5], "getoldannot": [4, 6], "getonebandinform": [9, 11, 26, 27], "getpalettecolor": [9, 11], "getpixel": [0, 1, 9, 11, 24, 25, 40, 41], "getpixelsizeinmet": [9, 11], "getpointatanotherscal": [9, 11], "getpreferredlevel": [9, 11, 32, 33, 36, 37], "getproj4str": [24, 25], "getpublicset": [0, 2], "getregion": [0, 1, 9, 11, 24, 25, 40, 41, 61], "getregionatanotherscal": [9, 11], "getsingletil": [9, 11], "getsingletileatanotherscal": [9, 11], "getsizeof": 10, "getsourcenamefromdict": [9, 11], "getstat": [0, 3, 9, 11, 24, 25, 38, 39, 40, 41, 42, 43, 48, 49, 50, 51], "gettesttil": [0, 2], "gettesttilesinfo": [0, 2], "getthumbnail": [0, 1, 2, 9, 11, 61, 70, 71], "gettiffdir": [44, 45], "gettil": [0, 1, 2, 9, 11, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 61], "gettilecach": [9, 10], "gettilecorn": [9, 11], "gettilecount": [9, 11], "gettileframesquadinfo": [9, 11], "gettileiotifferror": [44, 45], "gettilemimetyp": [9, 11], "gettilesinfo": [0, 2], "gettilesourc": [9, 11], "gettilespixel": [0, 2], "gettilesregion": [0, 2], "gettilesthumbnail": [0, 2], "gettilewithfram": [0, 2], "getvers": [4, 5], "getyamlconfigfil": [0, 2], "gff": 60, "ghcr": 67, "gheight": 11, "gibyt": 57, "gif": 60, "girder": [0, 2, 5, 11, 15, 17, 19, 20, 24, 26, 28, 30, 32, 34, 36, 38, 40, 44, 46, 48, 50, 54, 59, 61, 67, 68, 71, 72], "girder_cli": 70, "girder_large_imag": 65, "girder_large_image_annot": 65, "girder_plugin": 21, "girder_sourc": [16, 18, 21, 25, 27, 29, 31, 33, 35, 37, 39, 41, 45, 47, 49, 51], "girder_tilesourc": 3, "girdercli": 70, "girderid": [4, 5, 54], "girderplugin": [0, 4, 19], "girdersourc": [0, 3], "girdertilesourc": [0, 3, 15, 17, 19, 24, 28, 30, 32, 34, 36, 38, 40, 44, 46, 48, 50], "girderworkerpluginabc": 52, "git": 58, "github": [58, 61, 67, 69, 70, 71], "give": 70, "given": [0, 1, 2, 4, 5, 7, 10, 11, 19, 24, 26, 30, 32, 36, 40, 44, 54, 64, 68], "glom": 64, "glymur": 67, "go": 54, "googl": [69, 70, 71], "gpkg": 60, "grai": [11, 22, 73], "granular": 2, "grb": 60, "grb2": 60, "grc": 60, "grd": 60, "greater": [5, 11, 24, 40, 57, 68], "greatli": 62, "green": [11, 22, 42, 61, 70], "grei": 60, "greyscal": [11, 22, 73], "grib": 60, "grib2": 60, "grid": [5, 64], "griddata": [5, 54], "griddataschema": [4, 5], "gridwidth": [5, 54], "group": [0, 5, 7, 11, 54, 64, 67], "groupschema": [4, 5], "gsb": 60, "gta": 60, "gte": 5, "gti": 60, "gtx": 60, "guarante": [11, 15, 17, 19, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61], "guid": 67, "gvb": 60, "gwidth": 11, "gx": 11, "gxf": 60, "gy": 11, "gz": 60, "h": [5, 48, 50, 64, 66], "h5": 60, "ha": [0, 4, 5, 10, 11, 20, 22, 24, 54, 57, 60, 61, 62, 63, 64, 65, 67, 68, 70, 71, 72, 73], "had": 61, "half": [5, 11, 13, 22, 54, 73], "halfwai": 73, "ham": 11, "handl": [11, 56, 57, 58, 61, 66, 67], "handlecopyitem": [0, 3], "handlefilesav": [0, 3], "handler": [8, 46, 52], "handleremovefil": [0, 3], "handlernam": 0, "handlesettingsav": [0, 3], "happen": 61, "harmon": 61, "hasalpha": [5, 54], "hash": [0, 10, 11, 24, 38, 40, 42, 48, 50, 56], "hashsum": 70, "have": [0, 5, 11, 13, 15, 17, 19, 22, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 57, 58, 59, 61, 62, 64, 67, 68, 70, 73], "hdf": 60, "hdf5": 60, "hdr": 60, "head": [5, 54], "header": [13, 20, 62, 64], "heatmap": 5, "heatmapschema": [4, 5], "heavier": 67, "hed": 60, "heic": 60, "heif": 60, "height": [1, 5, 11, 13, 15, 19, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 64, 68, 70], "heightsubdivis": [5, 54], "help": [11, 54, 66], "here": [0, 54, 57, 68, 69, 73], "hex": 11, "hexadecim": [54, 64, 73], "hf2": 60, "hgt": 60, "hi": 60, "hidden": [5, 54], "hierarchi": 64, "hif": 60, "high": [5, 7, 9, 11, 12, 13, 17, 36, 44, 61, 72], "higher": [9, 12, 13, 26, 66, 73], "highest": [7, 11, 24, 32, 40, 72], "highi": 5, "highx": 5, "highz": 5, "hist": 11, "histogram": [0, 1, 9, 11, 73], "histogramthreshold": [9, 11], "histomicstk": [70, 71], "histomicsui": [62, 67], "histori": 5, "hole": [5, 54], "horizont": [11, 13, 61, 66], "host": 11, "how": [10, 54, 56, 57, 58, 62, 64, 73], "howev": [5, 70, 73], "hsl": 73, "hsv": 73, "hsv_valu": 71, "htd": 60, "html": 60, "http": [5, 11, 54, 58, 59, 61, 67, 68, 70, 71], "httpredirect": 20, "human": [7, 54], "huron": 70, "hx": 60, "hyperthread": 9, "i": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 70, 71, 72, 73, 74], "i2i": 60, "ic": 60, "icb": 60, "icc": [11, 56, 57, 73], "icc_correct": [0, 57], "icn": 60, "ico": 60, "icon": [60, 62, 64], "id": [0, 4, 5, 6, 7, 9, 11, 54, 60, 62, 63, 64, 68, 70, 72], "ident": 11, "identifi": [0, 64], "idofresourc": 70, "idregex": [4, 5], "idx": [11, 13], "ifd": 13, "ifdcount": 13, "ifdindic": 13, "ignor": [11, 24, 26, 40, 42, 48, 57, 61, 68], "ignored_path": 42, "iim": 60, "im": 60, "im3": 60, "imag": [0, 1, 2, 4, 5, 10, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 57, 58, 59, 62, 65, 68, 69, 71, 72, 74], "image2_jpeg2k": 70, "image_item": [0, 3], "imagebyt": [9, 11], "imagecm": [11, 57, 73], "imagecmsprofil": 11, "imagecolor": 11, "imagedata": [1, 11, 28], "imagedescript": [13, 50, 51, 66, 71], "imageframepreset": 64, "imageframepresetdefault": 64, "imagefunc": 1, "imageheight": [44, 45], "imageitem": [0, 1], "imagekei": [1, 11, 28, 50, 71], "imagekwarg": 11, "imagemim": [1, 11, 28], "imagenamefilt": 5, "imageri": 67, "imagewidth": [44, 45], "img": 60, "immedi": [11, 20, 64], "implement": [5, 11, 20, 46, 52, 56, 67], "impli": 68, "implicit": [9, 12], "implicit_high": [9, 12], "import": [0, 11, 20, 57, 59, 61, 63, 64, 70, 71], "importdata": [19, 20], "importlib": [70, 71], "improv": [11, 67], "imres": 11, "inact": 5, "inc": 67, "includ": [1, 2, 5, 7, 9, 11, 13, 20, 24, 40, 54, 56, 58, 61, 62, 64, 66, 67, 68, 69, 70, 71, 72], "includecolor": 11, "includetilerecord": 11, "inclus": [11, 20, 64, 66], "increas": [5, 11, 62, 66], "independ": 5, "index": [1, 5, 7, 11, 13, 22, 24, 40, 44, 54, 61, 64, 67, 68, 71, 73], "indexc": [11, 24, 40, 61], "indexi": 71, "indexrang": [11, 24, 40, 61, 71], "indexstrid": [11, 24, 40, 61, 71], "indext": [11, 24, 40, 61], "indexxi": [11, 24, 40], "indexz": [11, 24, 40, 61], "indic": [0, 4, 5, 7, 13, 19, 54, 61, 68], "individu": [2, 11, 56, 68], "ineffici": 66, "info": [0, 2, 4, 5, 11, 13, 19, 20], "inform": [1, 2, 5, 11, 13, 20, 24, 26, 40, 54, 57, 61, 70, 73], "infrar": 61, "ingest": 57, "ini": [36, 60, 64], "init": [11, 24, 26], "initi": [0, 1, 4, 5, 7, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52], "initupload": [19, 20], "injectannotationgroupset": [4, 5], "inprocess": 10, "input": [11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 66], "inputpath": 13, "inr": 60, "insensit": [5, 24, 26, 54, 61, 64], "insert": 13, "insert_on": 5, "insertlock": 5, "inspect": 70, "instal": [11, 22, 59, 66, 73, 74], "instanc": [0, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 57, 58, 61, 65, 67, 68, 70, 71, 72, 73, 74], "instead": [5, 11, 48, 54, 57, 58, 62, 64, 68, 70], "instruct": 58, "int": [5, 7, 9, 10, 11, 15, 17, 19, 20, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 57], "integ": [5, 11, 48, 54, 61, 64, 68, 72], "integr": [5, 67], "intend": [11, 46, 52, 57], "intens": 61, "intent": [11, 22, 57, 73], "intenum": 9, "interact": [11, 62, 70, 71], "intercept": [70, 71], "interest": 61, "interfac": [10, 11, 20, 62, 70], "intermedi": [11, 22, 73], "intern": [0, 4, 11, 19, 42, 44, 48, 50, 61, 66, 68, 70, 71, 72, 74], "internal_metadata": 64, "internalmetadataitemresourc": [0, 2], "interpol": [11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 73], "interpolateminmax": [26, 27], "interpret": [5, 11, 22, 24, 40, 48, 54, 61, 68, 70, 73], "invalid": 44, "invalidateloadmodelcach": [0, 3], "invalidoperationtifferror": [44, 45], "invers": 10, "invert": 19, "involv": 66, "io": [58, 61, 67, 70, 71], "ioopentifferror": [44, 45], "iotifferror": [44, 45], "ipl": 60, "iplmap": [9, 11, 70], "ipm": 60, "ipw": 60, "ipyleaflet": [11, 70, 71], "ipyleafletmap": 11, "ipyleafletmixin": [9, 11, 70, 71], "ipynb": 69, "ipython": 11, "is_geograph": [24, 26, 40], "is_geospati": [13, 14], "is_vip": [13, 14], "isbyteswap": 44, "ise": 5, "isg": 60, "isgeojson": [4, 7], "isgeospati": [9, 11, 24, 25, 40, 41], "ismsb2lsb": 44, "isn": [61, 70], "isnam": 7, "iso": 13, "isol": 67, "issu": [67, 74], "istil": 44, "istilecachesetup": [9, 10], "isupsampl": 44, "isvalidpalett": [9, 11], "item": [0, 1, 2, 5, 6, 7, 10, 11, 15, 17, 19, 20, 24, 26, 28, 30, 32, 34, 36, 38, 40, 44, 46, 50, 54, 62, 63, 67, 68, 70, 71, 72, 73, 74], "item_meta": [0, 3], "itemid": [2, 5, 64], "itemlist": 64, "itemlistdialog": 64, "itemmetadata": 64, "itemnameidselector": [4, 7], "iter": [7, 11, 64, 71], "iterator_rang": 11, "itertor": 7, "its": [0, 11, 54, 57, 61, 62, 64, 66, 70, 73], "itself": 61, "j": 66, "j2c": 60, "j2k": [34, 60], "java": 67, "javascript": 64, "jbig": 66, "jfif": [60, 73], "jl": 60, "job": [2, 52, 67], "joblogg": [52, 53], "jp2": [34, 60, 67], "jp2k": [13, 66, 67], "jpc": 60, "jpe": [38, 57, 60], "jpeg": [11, 13, 17, 22, 38, 44, 57, 60, 61, 66, 67, 73], "jpeg2000": 67, "jpegqual": [1, 11, 22, 24, 28, 40, 73], "jpegsubsampl": [1, 11, 22, 24, 28, 40, 73], "jpf": [34, 60], "jpg": [38, 57, 60, 61], "jpk": 60, "jpl": 60, "jpt": 60, "jpx": [34, 60], "json": [5, 11, 13, 20, 22, 26, 28, 54, 57, 59, 60, 64, 68, 73], "json_seri": [13, 14], "jsondict": [9, 11], "jsonresp": 70, "jsonschema": 68, "jupyt": [9, 12, 67, 71], "jupyter_host": [9, 11], "jupyter_proxi": [9, 11, 70, 71], "jupyterhub": 11, "jupyterhub_service_prefix": 11, "jupyterlab": [11, 70, 71], "just": [0, 1, 5, 11, 13, 61, 64, 68, 70, 71, 73], "jxl": 60, "k": 58, "kap": 60, "kea": 60, "keep": [1, 2, 5, 11, 22, 66, 73], "keepinactivevers": 5, "kei": [0, 1, 2, 5, 6, 7, 9, 10, 11, 13, 15, 19, 20, 22, 24, 26, 28, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 57, 62, 64, 66, 68, 72, 73], "kept": [1, 54, 62], "kernel": 11, "key0": 7, "key0kei": 7, "key1": 54, "key2": [7, 54], "keydict": 1, "keykei": 7, "keykey0": 7, "keyselector": [4, 7], "kitwar": [61, 65, 67, 70, 71], "klb": 60, "kml": 60, "kmz": 60, "know": [24, 40, 70], "known": [0, 1, 7, 11, 15, 17, 19, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 66, 67, 70], "kro": 60, "kwarg": [0, 1, 2, 4, 5, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 61, 73], "kwath": 11, "l": [48, 66, 70, 71], "l2d": 60, "la": 48, "lab": 70, "label": [5, 7, 13, 54, 60, 61, 62, 64, 66, 67], "labelposit": 13, "labelschema": [4, 5], "lanczo": 11, "larg": [0, 1, 2, 4, 5, 10, 11, 13, 52, 54, 57, 58, 61, 62, 64, 65, 66, 69, 72, 74], "large_imag": [0, 13, 17, 24, 40, 48, 55, 56, 57, 58, 60, 61, 62, 64, 66, 67, 68, 70, 71, 73], "large_image_": 57, "large_image_auto_set": [0, 3], "large_image_auto_use_all_fil": [0, 3], "large_image_cache_backend": 57, "large_image_cache_python_memory_port": 57, "large_image_cache_tilesource_maximum": 57, "large_image_config_fold": [0, 3], "large_image_convert": [50, 55, 66], "large_image_default_view": [0, 3], "large_image_exampl": 69, "large_image_icc_correct": [0, 3], "large_image_jupyter_proxi": 11, "large_image_max_small_image_s": [0, 3], "large_image_max_thumbnail_fil": [0, 3], "large_image_notification_stream_fallback": [0, 3], "large_image_resourc": [0, 3], "large_image_show_extra": [0, 3], "large_image_show_extra_admin": [0, 3], "large_image_show_extra_publ": [0, 3], "large_image_show_item_extra": [0, 3], "large_image_show_item_extra_admin": [0, 3], "large_image_show_item_extra_publ": [0, 3], "large_image_show_thumbnail": [0, 3], "large_image_show_view": [0, 3], "large_image_source_bioformat": 55, "large_image_source_deepzoom": 55, "large_image_source_dicom": 55, "large_image_source_dummi": 55, "large_image_source_gd": 55, "large_image_source_mapnik": 55, "large_image_source_multi": [55, 68], "large_image_source_nd2": 55, "large_image_source_ometiff": 55, "large_image_source_openjpeg": 55, "large_image_source_openslid": 55, "large_image_source_pil": 55, "large_image_source_rasterio": 55, "large_image_source_test": 55, "large_image_source_tiff": 55, "large_image_source_tifffil": 55, "large_image_source_vip": 55, "large_image_source_zarr": 55, "large_image_task": 55, "large_image_wheel": [58, 61, 67, 70, 71], "largeimag": [0, 74], "largeimageannotationplugin": [4, 8], "largeimagefil": [0, 15, 34], "largeimageplugin": [0, 3], "largeimageresourc": [0, 2], "largeimagetask": [52, 53], "larger": [1, 11, 24, 40, 56, 57, 61], "last": [5, 11, 20, 22, 48, 50, 64, 68, 73], "lastdirectori": 44, "lastli": 64, "later": [11, 22, 62, 64], "latest": [58, 67], "latitud": 70, "latlong": [24, 26, 40], "launch_tile_serv": [9, 11], "layer": [5, 9, 11, 24, 26, 40, 54], "layer_x_max": 11, "layer_x_min": 11, "layer_y_max": 11, "layer_y_min": 11, "layersr": 26, "layout": [64, 68], "lazili": [11, 61], "lazytiledict": [9, 11], "lbl": 60, "lcp": 60, "lead": 11, "leader": 24, "leak": 11, "least": [5, 10, 11, 22, 26, 54, 57, 61, 64, 67, 73], "leav": [11, 22], "left": [1, 5, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 62, 64, 68, 70, 71], "lei": 60, "len": [7, 71], "length": [5, 11, 20, 54, 64, 68], "less": [5, 7, 10, 11, 57, 61, 66, 68, 72], "lesser": 57, "let": [62, 70], "level": [0, 2, 5, 9, 11, 13, 15, 16, 17, 18, 19, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 54, 56, 57, 60, 61, 64, 66, 67, 70, 71], "level_i": 11, "level_x": 11, "level_x_max": 11, "level_x_min": 11, "level_y_max": 11, "level_y_min": 11, "leverag": 11, "lexic": 68, "lib": 40, "librari": [11, 19, 30, 34, 36, 44, 46, 50, 61, 62, 66, 67, 70, 73], "libtiff_ctyp": 73, "libvip": [48, 67], "lidata": 13, "lidesc": 13, "lif": [15, 60], "liff": 60, "lighten": [11, 22, 26, 73], "like": [5, 10, 11, 13, 24, 40, 54, 57, 59, 61, 62, 64, 67, 68, 70, 73], "lim": 60, "limit": [0, 5, 6, 11, 20, 54, 56, 57, 61, 64], "line": 54, "linear": [26, 73], "linecolor": [5, 54], "linestringtyp": [4, 7], "linewidth": [5, 54], "link": [0, 4, 19, 58, 61, 67, 69, 70, 71], "lint": 58, "lintclient": 58, "linux": [58, 61, 67, 70], "list": [1, 2, 4, 5, 7, 10, 11, 13, 15, 19, 20, 22, 24, 26, 28, 34, 36, 40, 42, 44, 46, 50, 52, 54, 57, 60, 62, 67, 68, 72, 73], "listextens": [9, 11], "listmimetyp": [9, 11], "listsourc": [0, 2, 9, 11, 60], "listtilesthumbnail": [0, 2], "ll": [61, 67, 70], "lm": 60, "load": [0, 3, 4, 5, 8, 10, 11, 19, 20, 21, 54, 57, 61, 70], "loadcach": [9, 10], "loadgirdertilesourc": [0, 3], "loadmodel": [0, 3], "loadmodelcach": 3, "local": [17, 66, 67, 71], "localhost": [58, 70, 71], "localjob": [1, 2], "locat": [5, 7, 11, 24, 40, 48, 50, 56, 57, 61, 72], "lock": 10, "lockkei": 1, "log": [2, 9, 10, 24, 40, 46, 52, 61, 64], "log2": 11, "logerror": [9, 10], "logger": [9, 10, 57], "logic": [9, 13, 66], "loginterv": 2, "logprint": [10, 57], "long": 54, "longer": 11, "longitud": 70, "longlat": 70, "look": [5, 54, 70], "lookup": [7, 70], "lose": 5, "lossi": [48, 50, 61, 66], "lossless": [13, 48, 50, 54, 66], "lost": 73, "low": [5, 7, 9, 11, 12, 15, 17, 19, 26, 30, 32, 36, 38, 46, 48, 50, 72], "lower": [9, 11, 12, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 66], "lowercas": 54, "lowest": [7, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 72], "lowi": 5, "lowx": 5, "lowz": 5, "lr": 70, "lrucachemetaclass": [9, 10], "lsm": 60, "lt": 5, "lzma": 66, "lzw": [11, 13, 66], "m": 26, "machin": [11, 56], "macro": [13, 61, 64], "made": [67, 68], "mag_pixel": 11, "magnif": [1, 11, 15, 19, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 68, 70, 71], "magnificaiton": [11, 24, 40], "mai": [5, 9, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 57, 61, 62, 64, 66, 73], "main": [5, 7, 24, 40, 44, 54, 56, 57, 61, 64, 67, 68, 73], "maintain": [5, 64, 67], "mainten": 58, "major": 54, "make": [1, 7, 11, 13, 22, 44, 57, 61, 66, 67, 73], "make_cr": [40, 41], "make_lay": [9, 11], "make_map": [9, 11], "make_vsi": [9, 11], "manag": [11, 58, 61, 65, 67], "mani": [1, 5, 10, 57, 58, 61, 70, 73], "manner": [57, 68], "mantissa": 11, "manual": [9, 12, 22, 42, 48], "map": [5, 9, 11, 22, 26, 48, 54, 60, 61, 64, 70, 73], "map1": 70, "map2": 70, "map3": 70, "map4": 70, "mapnik": [11, 26, 67], "mapnikfiletilesourc": [26, 27], "mapnikgirdertilesourc": [26, 27], "mark": 0, "markup": [5, 54], "mask": [48, 50, 61, 73], "maskband": [24, 40], "maskpixelvalu": [9, 11, 73], "mat": 60, "match": [0, 5, 11, 22, 57, 64, 66, 68, 72, 73], "matchsiz": 13, "matplotlib": [11, 22, 67, 73], "matrix": [5, 54], "max": [7, 11, 22, 24, 32, 36, 38, 40, 42, 57, 61, 64, 66, 70, 72, 73], "max_annotation_input_file_length": 57, "max_i": 61, "max_small_image_s": [0, 57], "max_thumbnail_fil": 0, "max_work": [11, 61], "max_x": 61, "max_z": 61, "maxannotationel": [4, 7], "maxcolor": [5, 54], "maxdefault": 38, "maxdetail": 5, "maxdistinct": [4, 7], "maxframes": 11, "maxheight": [11, 38], "maxi": 11, "maximum": [1, 5, 7, 10, 11, 22, 24, 38, 40, 42, 54, 56, 57, 61, 64, 66, 67, 70, 71, 72, 73], "maxitem": [4, 5, 7, 10, 54], "maxlevel": 42, "maxsiz": [9, 10, 38], "maxtextur": 11, "maxtextures": 11, "maxtotaltexturepixel": 11, "maxwidth": [11, 38, 61], "mayhaveadjacentfil": [0, 3, 15, 16, 34, 35], "mayredirect": [1, 38], "mbtile": 60, "mdb": 60, "mea": 60, "meain": [5, 54], "mean": [5, 9, 11, 24, 38, 40, 54, 60, 66, 70], "median": [11, 66], "medianfilt": [9, 11], "medic": [61, 67], "medium": [9, 11, 12, 15, 19, 28, 32, 34, 36, 44, 50], "mem": 60, "member": [11, 24, 40], "membership": 64, "memcach": [9, 12, 56, 57, 67], "memoiz": 10, "memori": [9, 10, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 57, 61, 67], "mercat": 61, "merg": 64, "messag": [2, 10, 57, 66], "meta": [0, 5, 7, 44, 64, 72], "metadata": [0, 5, 6, 7, 9, 11, 13, 15, 17, 19, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 62, 67, 68, 70, 71, 72], "metadatasearchhandl": [0, 3, 4, 8], "metakei": 0, "meter": [11, 61], "method": [0, 1, 5, 11, 15, 17, 19, 20, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 61, 64, 66, 67, 70], "methodcach": [9, 10], "metric": 66, "microscop": 61, "microscopi": 61, "middl": 61, "might": [7, 24, 59, 61, 68], "millimet": [11, 24, 40, 61], "mime": [0, 1, 11, 24, 28, 40, 64, 70], "mime_typ": 61, "mimetyp": [2, 9, 11, 15, 16, 17, 18, 19, 21, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 44, 45, 46, 47, 48, 49, 50, 51, 57, 60], "mimetypeswithadjacentfil": [0, 3, 36, 37], "min": [7, 11, 22, 24, 40, 42, 61, 64, 66, 70, 72, 73], "minageindai": 5, "mincolor": [5, 54], "minel": 5, "minheight": [48, 49], "minim": [11, 67], "minimizecach": [9, 12, 57], "minimum": [5, 11, 22, 32, 36, 42, 54, 57, 61, 64, 68, 70, 73], "minimums": 5, "minitem": [5, 54, 68], "minlength": [5, 54], "minlevel": 42, "minor": 54, "minut": 71, "minwidth": [48, 49], "mirax": [36, 60, 67], "misc": 11, "miss": [11, 22, 24, 56, 61], "mixin": 11, "mm": [1, 11, 15, 19, 28, 30, 32, 34, 36, 44, 46, 48, 50, 61], "mm_x": [11, 15, 19, 24, 28, 30, 32, 38, 40, 42, 44, 46, 48, 49, 50, 51, 61, 68, 70, 71], "mm_y": [11, 15, 19, 24, 28, 30, 32, 38, 40, 42, 44, 46, 48, 49, 50, 51, 61, 68, 70, 71], "mnc": 60, "mng": 60, "mod": 60, "modal": [59, 61], "modalitiesinstudi": 59, "mode": [0, 4, 7, 9, 19, 64, 66, 71, 73], "model": [0, 2, 3, 4, 8, 20, 62, 67], "moder": 11, "modern": 57, "modif": [5, 20], "modifi": [0, 5, 13, 20, 42, 61, 62, 64, 71, 73], "modified_til": 61, "modify_tiff_before_writ": [13, 14], "modify_til": 61, "modify_tiled_ifd": [13, 14], "modify_vips_image_before_output": [13, 14], "modul": [3, 8, 12, 14, 16, 18, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 61, 66, 73], "mongo": [2, 5, 74], "mongodb": [5, 58], "monochrom": [11, 42], "monoton": [5, 54], "more": [5, 9, 10, 11, 22, 54, 57, 61, 64, 66, 67, 70, 72, 73], "most": [5, 7, 11, 22, 48, 50, 54, 57, 61, 64, 66, 67, 70, 72, 73], "mostli": 54, "mount": 67, "mov": 60, "mpeg": 60, "mpg": 60, "mpl": 60, "mpo": 60, "mpr": 60, "mrc": 60, "mrf": 60, "mrw": 60, "mrx": [36, 57, 60], "msg": 10, "msp": 60, "msr": 60, "mtb": 60, "much": [54, 56], "multi": [11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 67], "multiband": 48, "multifiletilesourc": [28, 29], "multifram": [64, 66, 71], "multigirdertilesourc": [28, 29], "multilinestringtyp": [4, 7], "multipl": [5, 11, 22, 24, 40, 54, 56, 57, 63, 64, 66, 68, 73], "multipli": [11, 22, 73], "multipointtyp": [4, 7], "multipolygontyp": [4, 7], "multiprocess": [11, 61], "multiresolut": 67, "multisourceschema": 68, "must": [0, 5, 11, 13, 20, 22, 24, 26, 40, 48, 54, 58, 61, 62, 64, 72, 73], "mustbeavail": 10, "mustbetil": 44, "mustconvert": 7, "mutual": 71, "mvd2": 60, "my": 58, "myannotationnam": 54, "mydomain": 11, "n": [61, 67], "n1": 60, "naf": 60, "name": [0, 1, 2, 4, 5, 7, 9, 10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 54, 57, 60, 61, 62, 64, 67, 68, 71, 72, 73, 74], "namedcach": [9, 10], "namedtupl": 30, "namedtupletodict": [30, 31], "namematch": [9, 11, 19, 21], "namespac": [10, 73], "nat": 60, "nativ": [5, 24, 40], "nc": [26, 60], "nd": 60, "nd2": [30, 60, 67], "nd2filetilesourc": [30, 31], "nd2girdertilesourc": [30, 31], "ndarrai": 11, "ndpi": [36, 57, 60, 67], "nearest": [11, 24, 40, 66, 73], "nearli": [11, 66], "nearpoweroftwo": [9, 11], "necessari": [5, 20, 61], "necessarili": [57, 61], "need": [4, 5, 7, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 58, 61, 64, 67, 68, 70, 73, 74], "needslabel": 13, "nef": 60, "neg": [11, 48, 50], "neighbor": 11, "neither": [1, 11, 24, 40, 64, 67], "nest": [11, 62, 64, 72], "netcdf": 67, "never": [10, 11, 57], "new": [1, 2, 5, 9, 11, 13, 44, 48, 49, 50, 51, 54, 59, 61, 64, 65], "new_shap": 11, "new_sourc": 61, "newprior": [9, 11, 48, 49, 50, 51], "next": [2, 62, 64], "ngff": 67, "nhdr": 60, "ni": 67, "nia": 60, "nii": 60, "nitf": [11, 26, 60], "no_def": 70, "nocach": [11, 22], "nodata": [11, 22, 24, 40, 73], "node": 11, "nois": [66, 67], "non": [7, 11, 13, 20, 24, 40, 44, 50, 54, 66, 73], "none": [0, 1, 2, 4, 5, 6, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 57, 61, 64, 66, 70, 71, 72, 73], "nor": [1, 11, 24, 40, 64], "normal": [5, 11, 22, 54, 61, 73], "normalizerang": [5, 54], "nosav": 1, "note": [5, 11, 22, 24, 40, 48, 50, 54, 61, 64, 68, 73], "notebook": [11, 57, 67, 70, 71], "noth": 67, "notifi": 1, "notification_stream_fallback": 0, "notimplementederror": [46, 52], "novic": 5, "now": [5, 61], "np": 61, "np_max": [9, 11], "np_max_color": [9, 11], "np_mean": [9, 11], "np_median": [9, 11], "np_min": [9, 11], "np_min_color": [9, 11], "np_mode": [9, 11], "np_nearest": [9, 11, 50], "nparrai": 61, "nrrd": 60, "ntf": [11, 26, 60], "nuanc": 11, "nucleu": 72, "nucleus_circular": 72, "nucleus_radiu": 72, "null": [5, 7, 11, 22, 73], "number": [1, 2, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 57, 61, 62, 64, 66, 68, 72, 73], "numberinst": [4, 5], "numberofstrip": 44, "numer": [11, 22, 57, 68, 73], "numitem": 10, "numpi": [11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 61, 67, 68, 70, 71, 73], "numpyallow": [11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50], "numpyres": [9, 11], "o": [5, 9, 70, 71], "obf": 60, "obj": [13, 30], "obj1": 30, "obj2": 30, "object": [0, 1, 2, 5, 7, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 68, 73], "obsep": 60, "obtain": [54, 68, 72], "occur": [5, 11], "octet": 0, "odd": [5, 54], "off": [11, 54, 57, 61], "offer": 64, "offici": 58, "offset": [0, 5, 6, 11, 20, 24, 40, 48, 50, 54, 68], "often": [56, 61, 73], "oib": 60, "oif": 60, "oir": 60, "old": [5, 62], "oldvers": 5, "om": [32, 50, 60, 61, 67], "ometiff": [32, 67, 73], "ometifffiletilesourc": [32, 33], "ometiffgirdertilesourc": [32, 33], "omit": [5, 68], "onc": [11, 20, 61], "one": [2, 5, 7, 11, 13, 22, 24, 26, 40, 48, 50, 54, 56, 57, 58, 60, 61, 62, 64, 67, 68, 72, 73, 74], "ones": 48, "onhov": [5, 54], "onli": [0, 5, 7, 11, 20, 22, 24, 28, 30, 40, 44, 48, 50, 54, 56, 57, 58, 59, 61, 64, 66, 68, 72, 73], "onlyfram": [13, 66], "onlyinfo": 11, "onlylist": 1, "onlyminmax": 11, "onto": [5, 54], "opac": [5, 54], "opaqu": 11, "open": [5, 9, 11, 15, 16, 17, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 54, 57, 60, 61, 63, 65, 70, 71], "openjpeg": [34, 67, 73], "openjpegfiletilesourc": [34, 35], "openjpeggirdertilesourc": [34, 35], "openslid": [36, 67, 73, 74], "openslidefiletilesourc": [36, 37], "openslidegirdertilesourc": [36, 37], "oper": [56, 70], "opt": 67, "optim": [24, 40, 56, 66, 67], "option": [1, 5, 7, 11, 13, 20, 24, 26, 28, 40, 42, 48, 50, 54, 58, 61, 65, 66, 67, 70, 71, 72], "order": [0, 1, 11, 13, 22, 50, 57, 60, 61, 62, 64, 68], "org": [5, 54, 68], "origin": [5, 11, 13, 24, 30, 40, 48, 49, 54, 56, 61, 64, 66, 71, 73], "original_image_path": 71, "original_sourc": 61, "originalstyl": 73, "orthogon": 61, "osgeo_util": 24, "osx": [58, 61, 67], "other": [0, 5, 7, 11, 15, 17, 19, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 57, 58, 62, 64, 66, 67, 68, 70, 71, 72, 73], "otherwis": [0, 1, 5, 7, 9, 10, 11, 22, 24, 26, 40, 54, 57, 67, 72, 73], "our": [61, 67, 69, 70], "out": 63, "output": [7, 11, 13, 24, 40, 48, 50, 57, 61, 66, 67, 68, 73], "outputpath": 13, "outsid": [11, 22, 70, 73], "over": [62, 70], "overal": 7, "overlai": [5, 62, 67], "overlaid": 54, "overlap": [11, 61], "overlayschema": [4, 5], "overload": 11, "overrid": [1, 5, 11, 22, 52, 68, 73], "overwrit": [13, 66], "overwriteallow": [48, 50], "own": 1, "p": [58, 60, 61, 66, 68], "p1": 61, "packag": [3, 8, 12, 14, 16, 18, 21, 23, 25, 27, 29, 31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 58, 59, 61, 66, 67], "packbit": [13, 66], "page": [0, 4, 19, 55, 61, 62, 63, 64, 67], "pagin": 5, "pair": [5, 7, 11, 64], "palett": [11, 22, 61, 64, 67, 73], "palettebas": 73, "palm": 60, "panchromat": 61, "paper": 70, "par": 60, "parallel": 11, "param": [2, 6, 11, 13, 20, 68], "param1": 61, "paramet": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 68, 70, 71, 73], "parent": [5, 20, 64, 72], "parenttyp": 20, "pars": [11, 13, 56, 57, 68, 72], "parse_image_descript": [44, 45], "parseabl": [11, 22], "part": [0, 5, 11, 22, 24, 38, 40, 42, 48, 50, 54, 68], "partial": [5, 11, 20, 54, 61], "particular": [11, 15, 17, 19, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 57], "particularli": 67, "pass": [1, 5, 10, 11, 13, 50, 68, 71, 73], "password": [10, 57], "past": [64, 73], "patch": [48, 50], "patchlibtiff": [44, 45], "patchmount": [0, 3], "path": [0, 4, 11, 13, 15, 17, 19, 24, 26, 28, 30, 32, 34, 36, 38, 40, 44, 46, 48, 50, 58, 61, 66, 67, 68, 70], "pathologi": 61, "pathoruri": 11, "pathpattern": 68, "pattern": [5, 7, 54, 61, 67, 72], "patternproperti": 68, "pbm": 60, "pcd": 60, "pcoraw": 60, "pcx": 60, "pd": 60, "pdf": 60, "peak": 66, "penalti": 57, "per": [11, 24, 40, 61, 64, 66, 73], "percentag": 64, "perceptu": [57, 73], "perfect": 11, "perform": [7, 11, 20, 22, 57, 58, 67], "period": 5, "permiss": [0, 7, 64], "permit": [5, 50], "pfm": 60, "pgm": 60, "photograph": 61, "photoshop": 60, "physic": [9, 61], "pic": 60, "pick": [61, 73], "pickavailablecach": [9, 10], "pickl": 70, "pickleabl": 61, "picklecach": 1, "pict": 60, "piec": 7, "piecewis": 73, "pil": [11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 57, 67, 70, 73], "pil_bicub": [9, 11], "pil_bilinear": [9, 11], "pil_box": [9, 11], "pil_ham": [9, 11], "pil_lanczo": [9, 11, 50], "pil_max_enum": [9, 11], "pil_nearest": [9, 11], "pilfiletilesourc": [38, 39], "pilgirdertilesourc": [38, 39], "pilimageallow": [11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50], "pillow": 67, "pilres": [9, 11], "pip": [58, 70, 71], "pipelin": 73, "pix": 60, "pixel": [1, 5, 11, 13, 15, 19, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 61, 64, 66, 68, 70, 73], "pixelinfo": [44, 45], "pixelmap": [5, 48], "pixelmapcategoryschema": [4, 5], "pixelmapschema": [4, 5], "pixeltoproject": [9, 11, 24, 25, 40, 41], "place": [2, 63, 66], "placehold": 64, "plain": [11, 30], "plane": 61, "plasma_6": 73, "platform": [58, 65, 67], "pleas": 11, "plot": 72, "plottabl": [7, 65], "plottableitemdata": [4, 7, 72], "plu": [1, 11, 22, 64, 72], "plugin": [0, 4, 11, 19, 20, 52, 59, 61, 65, 67, 70, 73], "pluginset": [0, 3], "png": [11, 17, 22, 57, 60, 61, 68, 70, 73], "pnl": 60, "pnm": 60, "po": 5, "point": [5, 10, 11, 48, 50, 62, 67, 68, 72], "pointshapeschema": [4, 5], "pointtyp": [4, 7], "pole": 61, "polygon": [5, 54, 62, 67], "polygontyp": [4, 7], "polylin": 5, "polylineshapeschema": [4, 5], "polylinetyp": [4, 7], "pool": 61, "popul": [7, 10, 64, 68, 72], "port": 11, "portabl": 60, "portion": [10, 57, 61, 72], "posit": [5, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 66, 73], "posixpath": 11, "possibl": [20, 26, 57, 72], "possiblegirderid": 4, "possibli": [1, 11, 13, 54, 73], "post": [72, 73], "postband": 73, "postscript": 60, "power": [11, 42, 61], "ppi": 60, "ppm": 60, "pr3": 60, "prarm": 11, "pre": [67, 73], "preband": 73, "prebuilt": 58, "preced": 64, "precomput": 73, "predictor": [13, 66], "prefer": [9, 10, 11, 12, 15, 19, 26, 28, 30, 32, 34, 36, 44, 46, 50, 58, 73], "prefix": [2, 7, 11, 24, 26, 57, 59], "preparecopyitem": [0, 3], "prerequisit": 58, "present": [5, 7, 10, 11, 24, 30, 40, 42, 61, 62, 64, 68, 72, 73], "preserv": [1, 11, 24, 40], "prevent": 5, "previou": [5, 61, 62, 65, 68], "prf": 60, "primari": [11, 13, 22, 61, 64, 72], "print": [61, 62, 71], "prioriti": [11, 60], "probabl": [11, 61], "problem": 5, "process": [4, 11, 13, 20, 56, 57, 59, 60, 61, 67, 73], "process_annot": [4, 8], "process_til": 71, "processed_example_1": 71, "processed_image_path": 71, "processed_til": 71, "processor": 66, "produc": [11, 66], "profil": [11, 57, 73], "programmat": 11, "progress": [20, 73], "progresscontext": 20, "prohibit": 57, "proj": [24, 70], "proj4": [24, 26], "project": [9, 11, 24, 25, 26, 27, 40, 41, 67, 70], "projectionorigin": [9, 11, 24, 25, 26, 27, 40, 41], "prop": 7, "properti": [0, 4, 5, 7, 10, 11, 19, 20, 24, 26, 44, 48, 50, 54, 64, 68, 72], "provid": [0, 15, 17, 19, 24, 26, 28, 30, 32, 34, 36, 38, 40, 44, 46, 48, 50, 54, 59, 61, 67, 70], "proxi": [11, 70, 71], "psd": 60, "psnr": [13, 66], "psutil": [9, 56], "ptif": [44, 60], "ptiff": [44, 60], "public": 5, "publicflag": 5, "pull": [67, 70], "pure": 70, "purpos": [19, 72], "putyamlconfigfil": [0, 2], "px": 40, "pxr": 60, "py": [40, 73], "py310": 58, "py311": 58, "py39": 58, "pydicom": 19, "pypi": 67, "pyramid": [13, 66, 67], "pytest": 58, "python": [0, 4, 9, 11, 19, 56, 58, 59, 67, 68, 70, 72, 73], "pyvip": [48, 67], "q": 66, "qido": 59, "qoi": 60, "qpi": 60, "qptiff": [44, 60], "qualiti": [11, 13, 22, 66, 73], "qualnam": [5, 9, 11], "quarter": [11, 13, 22, 73], "queri": [0, 5, 11, 22, 61, 73], "quot": [11, 57], "r": [7, 54, 57, 58, 59, 72], "r3d": 60, "ra": 60, "radian": [5, 54], "radiu": [5, 54, 71, 72], "rais": [7, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52], "rang": [5, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 64, 70, 73], "rangevalu": [5, 54], "rangevalueschema": [4, 5], "rank": 11, "raster": 26, "rasterio": [11, 40, 70], "rasteriofiletilesourc": [40, 41], "rasteriogirdertilesourc": [40, 41], "rate": 64, "rather": [10, 64, 72], "ratio": [1, 11, 13, 24, 40, 64, 66], "rational": 70, "raw": [11, 60, 73], "rcpnl": 60, "rda": 60, "re": [5, 13, 60], "read": [0, 5, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 57, 60, 66, 67, 71, 73], "readabl": [7, 13, 54, 61], "reader": [44, 60], "readi": 58, "reason": [5, 57], "rec": 60, "receiv": [70, 71], "recent": [0, 5, 7, 11, 24, 38, 40, 42], "recommend": [13, 66, 68], "reconstruct": 5, "record": [0, 1, 5, 7, 11, 20, 46, 52, 62, 64, 72], "recordselector": [4, 7], "recreat": 58, "rectangl": [5, 70], "rectanglegrid": [5, 54], "rectanglegridshapeschema": [4, 5], "rectangleshapeschema": [4, 5], "rectangletyp": [4, 7], "rectangular": [11, 24, 40], "recurs": [2, 6, 72], "red": [11, 22, 42, 61, 70], "redi": [10, 57], "rediscach": [9, 12], "redoexist": 2, "reduc": [11, 57, 66], "refer": [0, 4, 5, 11, 61, 70, 73], "referenc": [61, 68, 72], "reflect": [0, 11, 24, 38, 40, 42, 48, 50], "regardless": [5, 11, 60, 70], "regex": [5, 64, 66], "regexp": 64, "region": [1, 5, 11, 24, 40, 54, 67, 70, 71], "region_i": 11, "region_x": 11, "region_x_max": 11, "region_y_max": 11, "regiondata": [1, 11, 24, 40], "regionheight": 1, "regionmim": 1, "regionwidth": 1, "regist": 73, "regular": [11, 54, 57, 68, 72], "rel": [0, 4, 17, 19, 68], "relat": [5, 72], "releas": [9, 11], "relev": [0, 11], "reli": 10, "remaind": 68, "remap": 73, "rememb": 58, "remot": [11, 24, 67, 70], "remotemetadata": 70, "remoteurl": 70, "remov": [0, 1, 4, 5, 10, 20, 24, 26], "removeel": [4, 5], "removeoldannot": [4, 5], "removeoldel": [4, 5], "removethumbnail": [0, 3], "removethumbnailfil": [0, 1], "removewithqueri": [4, 5], "render": [54, 62, 67], "reorder": 11, "replac": 64, "replace_on": 5, "report": [5, 11, 19, 22, 30, 56, 61, 66, 73], "repositori": [58, 67, 69], "repr": [10, 11], "repres": [5, 7, 54, 61, 64], "represent": 11, "request": [5, 9, 11, 38, 44, 61, 70, 72, 73], "requir": [1, 5, 11, 20, 24, 40, 54, 59, 61, 64, 68, 70], "requiredcolumn": 7, "requiredkei": 6, "resampl": [9, 12, 50], "resample_method": 11, "resamplemethod": [9, 11, 50], "rescal": 11, "reserv": 50, "resiz": 11, "resolut": [4, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 61, 66, 73], "resolv": [0, 4, 11, 72, 73], "resolveannotationgirderid": [4, 8], "reson": 67, "resourc": [0, 2, 5, 6, 11, 20, 54, 70, 72], "resourcefrommap2": 70, "resourcepath": 70, "respons": [0, 5, 11, 20, 64, 67], "rest": [0, 3, 4, 8, 11, 19, 21, 72], "restart": [2, 64], "restrict": [5, 11, 64], "restyl": 67, "result": [0, 1, 4, 7, 10, 11, 13, 22, 24, 40, 48, 50, 61, 63, 64, 68, 72, 73], "retil": [11, 67], "retriev": [1, 11, 28, 64], "return": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 68, 70, 71, 73], "returnfolderannot": [4, 6], "reus": [11, 22], "revent": 62, "revers": [5, 11], "revert": 5, "revertannotationhistori": [4, 6], "revertvers": [4, 5], "revisit": 57, "rewrit": 66, "rgb": [5, 11, 22, 42, 48, 54, 60, 61, 73], "rgba": [5, 11, 22, 48, 54, 60, 73], "right": [1, 5, 11, 54, 61, 62, 64, 68, 70, 73], "rik": 60, "rio": 40, "root": [0, 2, 11], "rotat": [4, 5, 7, 54], "rough": 57, "round": [11, 24, 40], "roundresult": [24, 40], "rout": 2, "row": [7, 44, 72], "rrespond": 5, "rrggbb": [11, 22, 54, 73], "rrggbbaa": [11, 22, 54, 73], "rst": [60, 64], "rsw": 60, "run": [61, 66, 67, 69, 70, 71], "s11": 68, "s12": 68, "s21": 68, "s22": 68, "safe": 57, "safeti": 5, "same": [5, 7, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 59, 61, 68, 70, 72, 73], "sampl": [11, 56, 66, 68, 73], "sampleoffset": 68, "samplescal": 68, "save": [0, 4, 5, 10, 11, 20, 48, 62, 64, 67, 71], "saveelementasfil": [4, 5], "scale": [1, 5, 11, 24, 40, 42, 54, 73], "scalewithzoom": [5, 54], "schedul": [9, 11], "schema": [5, 11, 62, 64, 67], "scheme": [26, 73], "scikit": 71, "scipi": 11, "scn": [36, 46, 60], "screen": 54, "sdat": 60, "sdt": 60, "search": [0, 5, 20, 46, 62, 64], "search_for_seri": 20, "searchmodel": 0, "second": [2, 7, 11, 30, 57, 73], "secondari": [61, 64], "section": [57, 61], "see": [0, 1, 5, 11, 15, 17, 19, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 58, 60, 62, 65, 66, 69, 70, 72, 73], "select": [7, 11, 59, 62, 64, 72], "selector": 7, "self": [1, 2, 5, 10, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50], "semant": [5, 54], "sent": [20, 57], "separ": [7, 11, 42, 54, 57, 61, 72], "seq": 60, "sequenc": [5, 11], "serial": [13, 61], "serializi": 13, "series_uid": 19, "serv": [11, 22, 58, 67, 70], "server": [11, 20, 57, 59, 64, 67, 71], "serverurl": 64, "servic": 65, "set": [0, 1, 5, 9, 10, 11, 13, 20, 24, 38, 40, 44, 46, 52, 54, 56, 57, 58, 68, 70, 71, 73, 74], "setaccesslist": [4, 5], "setconfig": [9, 12, 57], "setcontenthead": [19, 20], "setdirectori": 44, "setfolderannotationaccess": [4, 6], "setformat": [9, 11], "setlevel": 57, "setmetadata": [4, 5, 6], "setsubdirectori": 44, "setup": [58, 70], "sever": [61, 67, 71], "sg": 60, "sgi": 60, "sha512": 70, "shape": 61, "share": 56, "sharpen": 11, "shear": 54, "shift": [54, 68], "shortcut": 64, "shorthand": 73, "should": [1, 5, 10, 11, 13, 20, 52, 54, 56, 57, 63, 64, 73], "show": [5, 54, 61, 62, 64, 66, 67, 70, 71, 72], "show_extra": 0, "show_extra_admin": 0, "show_extra_publ": 0, "show_item_extra": 0, "show_item_extra_admin": 0, "show_item_extra_publ": 0, "show_thumbnail": 0, "show_view": 0, "shown": [5, 54, 64], "shrink": 66, "sid": 60, "side": [11, 67], "sif": 60, "sigdem": 60, "sign": 5, "signal": 66, "signifi": 57, "silent": 66, "similar": [64, 68], "similarli": 73, "simpl": [42, 67, 74], "simplenamespac": 11, "simpler": 68, "simpli": 20, "sinc": [9, 11, 57, 61, 63, 68], "singl": [1, 5, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 64, 66, 67, 73], "singleband": 68, "singular": 57, "sink": 69, "sixteen": 73, "size": [1, 5, 9, 10, 11, 13, 20, 24, 26, 38, 40, 42, 54, 57, 61, 62, 63, 64, 66, 67, 68, 70, 71, 73], "sizeeach": 10, "sizei": [9, 11, 15, 16, 17, 18, 19, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 61, 70, 71], "sizex": [9, 11, 15, 16, 17, 18, 19, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 61, 70, 71], "skill": [4, 5], "skimag": 71, "skip": [5, 7, 58, 73], "skipfileid": 1, "sld": 60, "slice": 68, "slide": [59, 61, 64, 67, 70], "slider": 71, "slightli": 5, "slippi": [11, 70], "slow": [24, 66], "slower": 73, "sm": 59, "sm2": 60, "sm3": 60, "small": [13, 54, 57, 61, 67, 73], "smaller": [9, 48, 50, 57, 70, 73], "so": [0, 5, 10, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 57, 61, 68, 70, 71], "solid": 73, "some": [1, 7, 11, 13, 20, 24, 28, 40, 54, 57, 58, 59, 61, 62, 63, 64, 66, 67, 69, 70, 71, 72, 73], "someth": [7, 10, 59, 61, 68], "sometim": 61, "sort": [0, 1, 5, 6, 7, 62, 64, 68], "sortabl": [62, 64], "sortdir": [5, 6], "sourc": [0, 1, 2, 4, 5, 6, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 55, 56, 57, 59, 60, 61, 63, 65, 66, 67, 71, 74], "source_2": 71, "source_bioformats_ignored_nam": 57, "source_metadata": 71, "source_pil_ignored_nam": 57, "source_vips_ignored_nam": 57, "sourcebound": 70, "sourcedict": 10, "sourcelevel": [9, 11, 24, 25, 26, 27, 40, 41, 70], "sourcenam": [68, 74], "sourceprior": [9, 11, 12, 15, 17, 19, 22, 26, 28, 30, 32, 34, 36, 38, 42, 44, 46, 48, 50, 60], "sourceregion": 11, "sourcescal": 11, "sourcesizei": [9, 11, 24, 25, 26, 27, 40, 41, 70], "sourcesizex": [9, 11, 24, 25, 26, 27, 40, 41, 70], "sourceunit": 11, "space": [5, 11, 24, 26, 40, 54, 61, 66, 73], "spam": 10, "sparsefallback": [11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50], "spatial": [5, 54], "spc": 60, "spe": 60, "spec": 2, "special": [48, 61, 64, 68], "specif": [2, 5, 11, 13, 15, 17, 19, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 57, 58, 59, 61, 64, 67, 68, 73, 74], "specifi": [0, 5, 10, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 57, 58, 60, 61, 62, 64, 66, 67, 68, 71, 72, 73], "speed": [67, 70, 71], "spent": [70, 71], "spi": 60, "split": [5, 11, 73], "spread": 54, "sqlite": [60, 71], "sqrt": 11, "squar": [11, 42, 61, 66], "sr": [24, 70], "srgb": 73, "st": 60, "stage": [0, 4, 19, 61, 73], "stain": [62, 64], "standalon": 65, "standard": [5, 11, 57, 59, 66, 68, 73], "starmap": 61, "start": [5, 9, 11, 13, 20, 26, 50, 58, 67], "stat": [66, 67], "state": [0, 11, 24, 38, 40, 42, 48, 50], "static": [0, 7, 10, 11, 20, 24, 26, 38, 40, 42, 69, 71], "statist": [1, 11, 24, 40, 67], "statu": 2, "stdev": [11, 24, 40, 70], "step": [5, 11, 20, 54, 68], "still": [5, 11, 70], "stk": 60, "stop": 26, "store": [0, 1, 5, 9, 13, 56, 58, 61, 66, 67], "stp": 60, "str": [5, 7, 9, 10, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 57], "stream": [0, 20], "strhash": [9, 10], "strict": [24, 40, 66], "strictli": [5, 54, 61], "stride": [11, 68], "string": [0, 2, 4, 5, 7, 10, 11, 13, 19, 20, 22, 24, 26, 38, 40, 42, 46, 48, 50, 54, 57, 64, 68, 72, 73], "strip": 24, "stroke": [5, 54], "strokecolor": [5, 54], "structur": [7, 54, 64, 72], "studi": 20, "study_uid": 19, "style": [9, 11, 22, 26, 56, 57, 63, 64, 67, 68, 70], "stylefunc": [9, 12, 73], "styleindex": 73, "sub": 13, "subclass": [1, 5, 46, 52], "subdirectori": 44, "subdirectorynum": 44, "subdivis": 54, "subifd": [13, 66], "subject": [5, 54], "submodul": [3, 8, 12, 14, 16, 18, 21, 25, 27, 29, 31, 33, 35, 37, 39, 41, 45, 47, 49, 51, 53], "suboptim": 57, "subpackag": [3, 8, 12, 21], "subparamet": 11, "subsampl": [11, 22], "subsequ": [5, 57, 61], "subset": [5, 30], "substanti": 57, "substr": 0, "subtoken": 5, "success": [13, 20], "suffici": 11, "suffix": [13, 58], "suit": 58, "sum": 5, "summar": 72, "summari": 70, "super": 5, "superpixel": [5, 54], "support": [5, 11, 44, 54, 57, 61, 62, 64, 66, 67, 70, 73], "supportsindex": 11, "suppos": 11, "sure": 13, "surround": 57, "sv": [13, 36, 44, 57, 60, 64, 66, 67, 70, 71, 74], "svslide": [36, 60], "switch": [58, 71], "sxm": 60, "symmetr": 11, "synthes": [56, 61], "system": [5, 9, 17, 54, 56, 57, 58, 67, 70], "szi": 60, "t": [1, 5, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 58, 61, 64, 66, 68, 70, 71], "t_po": 61, "tabl": [62, 64], "tag": [5, 13, 46], "tail": 54, "take": [7, 11, 13, 22, 42, 46, 52, 61, 66, 71, 73], "taken": [10, 62, 64], "tall": 70, "target": [11, 61, 67], "targetscal": 11, "targetunit": 11, "task": [53, 58, 61, 66, 67], "task_import": [52, 53], "tc": 60, "tc_ng_sfbay_us_geo_cog": 70, "tcga": 70, "tell": 61, "templat": 11, "temporari": [13, 66], "temppath": 13, "ter": 60, "terminologi": 61, "ters": 68, "tertiari": 64, "test": [42, 67, 70], "test_ori": 68, "test_orient1": 68, "test_orient2": 68, "test_orient3": 68, "test_orient4": 68, "test_orient5": 68, "test_orient6": 68, "test_orient7": 68, "test_orient8": 68, "testfromtiffrgbjpeg": 58, "testtilesourc": [42, 43], "text": [11, 46, 62, 64], "textur": 11, "tf2": 60, "tf8": 60, "tfr": 60, "tga": 60, "th": [5, 11], "than": [1, 5, 7, 10, 11, 24, 40, 54, 56, 57, 61, 64, 67, 68, 70, 72, 73], "thei": [0, 1, 5, 11, 20, 24, 40, 52, 54, 57, 58, 61, 63, 64, 66, 68], "them": [0, 1, 4, 5, 10, 11, 57, 58, 61, 66, 70], "themselv": 73, "thi": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 57, 58, 59, 61, 62, 64, 66, 67, 68, 70, 71, 72, 73, 74], "thick": 61, "thing": [5, 54], "third": [57, 61], "those": [7, 10, 11, 58, 59, 61, 70, 73], "though": [5, 11, 24, 40, 73], "thread": [2, 5, 11, 61], "threadpool": 61, "three": [54, 68, 71, 72], "threshold": [11, 73], "throttl": 10, "through": [11, 57, 59, 61, 62, 64, 71], "throw": [5, 13, 20], "thrown": 5, "thumb": 61, "thumbdata": [1, 11], "thumbmim": [1, 11], "thumbnail": [1, 2, 11, 13, 63, 64, 66, 67, 70, 71], "tif": [11, 26, 32, 36, 44, 46, 57, 60, 68, 70], "tiff": [11, 13, 22, 26, 32, 36, 44, 46, 57, 60, 61, 66, 67, 68, 71, 73], "tiff_adobe_defl": 11, "tiff_lzw": 11, "tiff_read": 45, "tiffcompress": [1, 11, 22, 24, 28, 40, 73], "tifferror": [44, 45], "tifffil": [46, 67, 70, 73], "tifffilefiletilesourc": [46, 47], "tifffilegirdertilesourc": [46, 47], "tifffiletilesourc": [32, 44, 45, 71], "tiffgirdertilesourc": [44, 45], "tifftool": 13, "tile": [0, 1, 3, 5, 9, 10, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 57, 60, 63, 64, 66, 67, 68, 69, 70, 74], "tile0": 61, "tile002": 61, "tile_format_": [11, 73], "tile_format_imag": [11, 24, 40, 73], "tile_format_numpi": [11, 24, 40, 61], "tile_format_pil": [11, 24, 40], "tile_fram": 11, "tile_height": 11, "tile_i": 11, "tile_magnif": 11, "tile_mm_i": 11, "tile_mm_x": 11, "tile_offset": 11, "tile_overlap": [11, 61], "tile_posit": 11, "tile_s": [11, 61], "tile_sourc": 11, "tile_width": 11, "tile_x": 11, "tilecach": 10, "tilecacheconfigurationerror": [9, 12], "tilecacheerror": [9, 12], "tiledict": [9, 12], "tiledoutput": 67, "tiledtiffdirectori": [44, 45], "tilefram": [0, 1, 2, 9, 11], "tileframesquadinfo": [0, 2], "tilegeneralerror": [9, 11, 12], "tilegeneralexcept": [9, 11, 12], "tileheight": [9, 11, 15, 16, 17, 18, 19, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 61, 68, 70, 71], "tileinfo": 11, "tileiter": [9, 12, 24, 40, 61, 71], "tileiteratoratanotherscal": [9, 11], "tilelock": 10, "tileoutputmimetyp": 11, "tiles": [13, 66], "tileset": 61, "tilesitemresourc": [0, 2], "tilesourc": [0, 1, 9, 12, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 57, 61, 70, 71, 73], "tilesource_opt": 64, "tilesourceassetstoreerror": [9, 11, 12], "tilesourceassetstoreexcept": [9, 11, 12], "tilesourceerror": [9, 11, 12], "tilesourceexcept": [9, 11, 12], "tilesourcefilenotfounderror": [9, 11, 12], "tilesourceinefficienterror": [9, 12, 24, 40], "tilesourcexyzrangeerror": [9, 12], "tilewidth": [9, 11, 15, 16, 17, 18, 19, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 61, 68, 70, 71], "time": [2, 5, 11, 22, 24, 40, 57, 61, 62, 66, 70, 71, 74], "tissu": 61, "titl": [5, 7, 54, 62, 64, 72], "tmp": 61, "tnb": 60, "to_map": [9, 11, 70], "toc": 60, "todo": 11, "togeth": [1, 11, 22, 54, 64, 67], "token": [0, 1, 70], "toler": 11, "tonativepixelcoordin": [9, 11, 24, 25, 40, 41], "too": [61, 64, 70], "tool": 58, "tooltip": 64, "top": [1, 5, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 60, 61, 68, 70], "tornado": 11, "tostr": 11, "total": [2, 5, 9, 11, 56, 68, 70, 71], "total_memori": [9, 12], "tox": [58, 70], "tpkx": 60, "trailer": 24, "transact": 5, "transform": [5, 54, 56, 67, 68], "transformarrai": [4, 5], "transit": 11, "translat": [54, 68], "transpar": [11, 22, 70, 73], "treat": [5, 11, 24, 40, 54, 68], "tree": [46, 64], "tri": 60, "trigger": 5, "trim": 61, "triplet": 54, "true": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 64, 68, 70, 71, 73], "try": [9, 11, 13, 58], "tset": 68, "tstep": 68, "tupl": [1, 5, 10, 11, 13, 24, 26, 40, 42, 54, 70, 73], "turn": [11, 60], "tvalu": 68, "two": [10, 11, 22, 30, 42, 54, 56, 61, 64, 67, 68, 72, 73], "txt": [58, 60], "type": [0, 1, 5, 7, 9, 10, 11, 20, 22, 24, 28, 40, 44, 54, 57, 61, 62, 64, 67, 68, 70, 72, 73], "typic": [11, 61, 71, 73], "u": [11, 70], "ui": [64, 67], "uint": 11, "uint16": [11, 22, 42, 71, 73], "uint8": [11, 42, 50, 70, 71, 73], "ul": 70, "unbindgirdereventsbyhandlernam": [0, 3], "undefin": 57, "under": 5, "underli": 11, "uniformsourc": 68, "union": 42, "uniqu": [11, 24, 40, 54, 61, 64], "unit": [1, 11, 24, 40, 61], "unitsacrosslevel0": [9, 11, 24, 25, 26, 27, 40, 41], "unitsperpixel": [24, 26, 40], "unknown": [9, 11, 61], "unless": [5, 10, 11, 54], "unlik": [0, 4, 19], "unload": 11, "unmodifi": 20, "unnecessari": 68, "unnecessarili": 11, "unread": 67, "unset": [11, 22, 64, 73], "unsharp_mask": 71, "unspecifi": [11, 54], "unstyl": 56, "until": 60, "up": [5, 54, 58, 61, 62, 64, 67, 70], "updat": [0, 5, 20, 62, 74], "update_fram": [9, 11], "updateannot": [4, 5, 6], "updateannotationaccess": [4, 6], "updatedid": [5, 62], "updateel": [4, 5], "updateelementchunk": [4, 5], "updatemani": 74, "updatemetadatakei": [0, 2], "updateus": 5, "upgrad": 65, "upload": [20, 57, 63, 70, 71], "upper": [5, 48, 50, 54, 61, 68], "uppercas": 57, "upsampl": 68, "ur": 70, "uri": 11, "url": [10, 11, 57, 59, 70], "us": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 19, 20, 22, 24, 26, 28, 38, 40, 42, 48, 50, 54, 56, 57, 58, 59, 61, 62, 63, 64, 66, 67, 68, 69, 72, 73], "usabl": 9, "usag": [57, 61], "user": [0, 1, 4, 5, 6, 7, 11, 19, 20, 44, 54, 58, 62, 63, 64, 72], "usernam": [10, 11, 57], "userschema": [4, 5], "userstain": 64, "usual": [11, 61], "utf": 68, "utf8": 11, "util": [4, 8, 9, 12, 20, 57, 67, 70, 71, 72], "v": [60, 61, 66, 67], "v1": [64, 70, 71], "val": [42, 57], "val1": 11, "val2": 11, "valid": [4, 5, 9, 11, 20, 24, 40, 44, 54, 57, 64], "validateboolean": [0, 3, 4, 8], "validatebooleanoral": [0, 3], "validatebooleanoriccint": [0, 3], "validatecog": [24, 25, 40, 41], "validatedefaultview": [0, 3], "validatedictorjson": [0, 3], "validatefold": [0, 3], "validateinfo": [19, 20], "validatenonnegativeinteg": [0, 3], "validationexcept": 5, "validationtifferror": [44, 45], "validatorannot": [4, 5], "validatorannotationel": [4, 5], "valu": [0, 1, 2, 4, 5, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 56, 57, 61, 62, 64, 66, 68, 71, 72, 73], "value1": 54, "vanilla": 11, "vari": [5, 11, 24, 40, 57, 73], "variabl": [11, 68], "variant": [11, 50, 73], "varieti": [57, 61, 66, 67, 70, 73], "variou": [61, 66], "vda": 60, "vector": 67, "verbos": 66, "veri": [5, 9, 66, 70], "version": [5, 6, 11, 46, 52, 58, 62, 65, 66, 69], "versionlist": [4, 5], "versu": 9, "vertic": [11, 13, 61], "vff": 60, "via": [7, 11, 15, 54, 57, 58, 59, 61, 64, 67, 68, 70, 72, 73], "video": 60, "view": [5, 11, 24, 40, 54, 57, 63, 64, 65, 69, 70, 73], "viewd": 63, "viewer": [5, 54, 61, 64, 69, 70, 71], "vip": [13, 48, 57, 60, 61, 67], "vips_kwarg": 48, "vipsfiletilesourc": [48, 49], "vipsgirdertilesourc": [48, 49], "viridi": [11, 73], "viridis_12": 11, "virtual": [10, 57], "visibl": [5, 54, 73], "visit": [61, 65, 69], "visual": [11, 61], "vm": [36, 60, 67], "vmu": [36, 60], "vnd": [50, 60], "volum": 67, "vrt": [11, 26, 60], "vsi": [15, 57, 60], "vst": 60, "vw": 60, "w": [48, 50, 66], "w_": 5, "wa": [5, 11, 20, 24, 40, 56, 61], "wado": 59, "wai": [58, 70, 73], "want": [11, 61, 67], "warn": [24, 40], "wat": 60, "wb": 61, "we": [0, 5, 11, 22, 24, 38, 40, 42, 61, 70, 71], "weakli": [5, 54], "web": [0, 4, 11, 19, 61, 65], "web_client": [0, 4, 19], "webp": [13, 60, 66], "webserv": 11, "weight": 11, "well": [11, 54, 56, 67, 68, 70, 73], "were": [4, 5, 20], "wgs84": [11, 70], "what": [5, 11, 56, 61, 70], "whatev": [46, 52], "wheel": [58, 70], "when": [0, 4, 5, 7, 11, 13, 19, 20, 22, 24, 38, 40, 42, 48, 50, 54, 57, 60, 61, 63, 64, 66, 67, 70, 71, 72, 73], "whenev": [20, 62], "where": [5, 7, 11, 13, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 57, 61, 62, 64, 67, 72, 73], "whether": [5, 11, 20, 54, 70], "which": [0, 1, 2, 5, 9, 10, 11, 13, 20, 22, 24, 26, 40, 48, 54, 57, 61, 64, 67, 68, 71, 73], "while": [0, 4, 19], "white": [11, 61, 68], "who": 5, "whole": [11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 61, 67, 70], "whose": [54, 64], "wide": [57, 67, 70], "wider": 70, "width": [1, 5, 11, 13, 15, 19, 24, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 64, 68, 70], "widthcount": 42, "widthsubdivis": [5, 54], "wildli": 56, "window": [61, 67], "wish": [61, 68, 70], "within": [5, 11, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 56, 61, 64, 72, 73], "without": [11, 13, 19, 57, 58, 67, 68, 73], "wlz": 60, "wmf": 60, "won": 57, "word": 50, "work": [5, 11, 19, 61, 64, 67, 68, 73], "worker": [11, 67], "world": [61, 70], "would": [0, 1, 5, 10, 11, 64, 73], "wpi": 60, "wrap": [7, 10], "wrapkei": [9, 10, 11], "wrapper": 11, "write": [48, 49, 50, 51, 66, 67], "write_to_fil": 48, "writer": 61, "written": [13, 66, 67, 71], "wsi": [20, 61, 67], "x": [1, 2, 5, 7, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 58, 60, 61, 63, 64, 67, 68, 70, 71, 72, 73], "x0": 7, "x1": 7, "xandi": 2, "xbm": 60, "xdce": 60, "xferd": [70, 71], "xmax": [11, 70], "xmin": [11, 70], "xml": [11, 17, 60], "xml_string": 11, "xoffset": [5, 54], "xpixmap": 60, "xpm": 60, "xqd": 60, "xqf": 60, "xstep": 68, "xv": 60, "xxx": [61, 67], "xy": [11, 24, 40, 42, 60, 61, 68], "xyset": 68, "xystep": 68, "xyvalu": 68, "xyz": 60, "y": [1, 2, 5, 7, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 61, 63, 66, 68, 70, 71, 72, 73], "y0": 7, "y1": 7, "yaml": [0, 28, 57, 60, 68], "yaml_config": 0, "yamlconfigfil": [0, 3], "yamlconfigfilewrit": [0, 3], "ycbcr": 73, "ye": [13, 66], "yet": [0, 11], "yield": [1, 5, 11, 20], "yieldel": [4, 5], "ymax": [11, 70], "ymin": [11, 70], "yml": [28, 57, 60], "yoffset": [5, 54], "you": [5, 11, 58, 61, 64, 67, 68, 69, 70, 71, 74], "your": [58, 61, 64, 67, 70], "z": [1, 2, 5, 7, 11, 15, 17, 19, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 54, 60, 61, 63, 70, 73], "z1": 68, "zarr": [50, 60, 61, 67, 69], "zarr_sink_exampl": 69, "zarrai": [50, 60], "zarrfiletilesourc": [50, 51, 71], "zarrgirdertilesourc": [50, 51], "zattr": [50, 60], "zero": [11, 57, 61, 66, 68], "zfp": 60, "zfr": 60, "zgroup": [50, 60], "zif": 60, "zip": [13, 50, 60, 61, 66, 71], "zoom": [5, 54, 67], "zoomabl": [70, 71], "zset": 68, "zstd": [13, 66], "zstep": 68, "zvalu": 68, "zvi": 60, "zxy": [11, 63, 70]}, "titles": ["girder_large_image package", "girder_large_image.models package", "girder_large_image.rest package", "girder_large_image", "girder_large_image_annotation package", "girder_large_image_annotation.models package", "girder_large_image_annotation.rest package", "girder_large_image_annotation.utils package", "girder_large_image_annotation", "large_image package", "large_image.cache_util package", "large_image.tilesource package", "large_image", "large_image_converter package", "large_image_converter", "large_image_source_bioformats package", "large_image_source_bioformats", "large_image_source_deepzoom package", "large_image_source_deepzoom", "large_image_source_dicom package", "large_image_source_dicom.assetstore package", "large_image_source_dicom", "large_image_source_dummy package", "large_image_source_dummy", "large_image_source_gdal package", "large_image_source_gdal", "large_image_source_mapnik package", "large_image_source_mapnik", "large_image_source_multi package", "large_image_source_multi", "large_image_source_nd2 package", "large_image_source_nd2", "large_image_source_ometiff package", "large_image_source_ometiff", "large_image_source_openjpeg package", "large_image_source_openjpeg", "large_image_source_openslide package", "large_image_source_openslide", "large_image_source_pil package", "large_image_source_pil", "large_image_source_rasterio package", "large_image_source_rasterio", "large_image_source_test package", "large_image_source_test", "large_image_source_tiff package", "large_image_source_tiff", "large_image_source_tifffile package", "large_image_source_tifffile", "large_image_source_vips package", "large_image_source_vips", "large_image_source_zarr package", "large_image_source_zarr", "large_image_tasks package", "large_image_tasks", "Annotation Schema", "API Documentation", "Caching in Large Image", "Configuration Options", "Developer Guide", "DICOMweb Assetstore", "Image Formats", "Getting Started", "Girder Annotation Configuration Options", "Caching Large Image in Girder", "Girder Configuration Options", "Girder Integration", "Image Conversion", "Large Image", "Multi Source Schema", "Jupyter Notebook Examples", "Using Large Image in Jupyter", "Using the Zarr Tile Sink", "Plottable Data", "Tile Source Options", "Upgrading from Previous Versions"], "titleterms": {"2": 74, "3": 74, "A": [54, 68], "To": 68, "With": 68, "across": 61, "all": 54, "an": 61, "annot": [5, 6, 54, 62], "annotationel": 5, "api": 55, "appli": 73, "arrow": 54, "assetstor": [20, 59], "associ": 61, "attribut": 71, "ax": 61, "band": 61, "base": [10, 11], "basic": 70, "cach": [10, 56, 63], "cache_util": 10, "cachefactori": 10, "categor": 73, "chang": 61, "channel": [61, 73], "circl": 54, "color": [54, 61, 73], "colormap": 73, "command": 66, "compon": 54, "composit": [68, 73], "conda": [61, 67], "config": 9, "configur": [57, 62, 64], "constant": [0, 4, 9], "content": [0, 1, 2, 4, 5, 6, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 55, 65, 67], "convers": 66, "coordin": 54, "correct": 73, "data": [54, 71, 72, 73], "default": 64, "develop": 58, "dicom_metadata": 19, "dicom_tag": 19, "dicomweb": 59, "dicomweb_assetstore_adapt": 20, "dicomweb_util": 19, "docker": 67, "document": 55, "download": 71, "edg": 73, "edit": [64, 71], "element": 54, "ellips": 54, "encod": 73, "environ": [57, 58], "exampl": [68, 69, 73], "except": [9, 44], "extens": 60, "file": [64, 70, 71], "fill": 73, "format": [60, 73], "format_aperio": 13, "frame": [61, 64, 68, 73], "framedelta": 73, "from": [57, 74], "full": [54, 68], "gamma": 73, "gener": [62, 64], "geo": 11, "geospati": 70, "get": 61, "girder": [57, 58, 62, 63, 64, 65, 70, 74], "girder_large_imag": [0, 1, 2, 3], "girder_large_image_annot": [4, 5, 6, 7, 8], "girder_plugin": 19, "girder_sourc": [15, 17, 19, 24, 26, 28, 30, 32, 34, 36, 38, 40, 44, 46, 48, 50], "girder_tilesourc": 0, "green": 73, "grid": 54, "guid": 58, "handler": 4, "heatmap": 54, "highlight": 67, "histori": 62, "imag": [54, 56, 60, 61, 63, 64, 66, 67, 70, 73], "image_item": 1, "indic": 67, "instal": [58, 61, 67, 70, 71], "integr": 65, "item": 64, "item_meta": 2, "iter": 61, "jupyt": [11, 69, 70], "larg": [56, 63, 67, 70], "large_imag": [9, 10, 11, 12], "large_image_config": [62, 64], "large_image_convert": [13, 14], "large_image_resourc": 2, "large_image_source_bioformat": [15, 16], "large_image_source_deepzoom": [17, 18], "large_image_source_dicom": [19, 20, 21], "large_image_source_dummi": [22, 23], "large_image_source_gd": [24, 25], "large_image_source_mapnik": [26, 27], "large_image_source_multi": [28, 29], "large_image_source_nd2": [30, 31], "large_image_source_ometiff": [32, 33], "large_image_source_openjpeg": [34, 35], "large_image_source_openslid": [36, 37], "large_image_source_pil": [38, 39], "large_image_source_rasterio": [40, 41], "large_image_source_test": [42, 43], "large_image_source_tiff": [44, 45], "large_image_source_tifffil": [46, 47], "large_image_source_vip": [48, 49], "large_image_source_zarr": [50, 51], "large_image_task": [52, 53], "line": 66, "list": 64, "loadmodelcach": 0, "local": 70, "log": 57, "memcach": 10, "metadata": [61, 64], "migrat": 74, "mime": 60, "miss": 73, "model": [1, 5], "modul": [0, 1, 2, 4, 5, 6, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 67], "mongo": 58, "multi": 68, "multipl": 61, "new": 71, "nodej": 58, "notebook": 69, "npm": 58, "option": [57, 62, 64, 73], "other": 61, "overlai": 54, "packag": [0, 1, 2, 4, 5, 6, 7, 9, 10, 11, 13, 15, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52], "paramet": 57, "pip": [61, 67], "pixelmap": 54, "plottabl": 72, "plugin": [57, 62, 64], "point": 54, "polylin": 54, "posit": 68, "prefer": 60, "preset": 64, "previou": 74, "process": 71, "project": 61, "properti": 61, "python": [57, 66], "read": 61, "rectangl": 54, "red": 73, "rediscach": 10, "region": 61, "requir": 58, "resampl": 11, "rest": [2, 6, 20], "result": 71, "run": 58, "sampl": [54, 61, 71], "scale": [61, 68], "schema": [54, 68], "serv": 61, "server": 70, "set": [62, 64], "sever": 73, "shape": 54, "singl": 68, "sink": 71, "sourc": [58, 68, 70, 73], "start": 61, "store": 62, "style": [61, 73], "stylefunc": 11, "submodul": [0, 1, 2, 4, 5, 6, 9, 10, 11, 13, 15, 17, 19, 20, 24, 26, 28, 30, 32, 34, 36, 38, 40, 44, 46, 48, 50, 52], "subpackag": [0, 4, 9, 19], "swap": 73, "tabl": 67, "task": 52, "test": 58, "three": 73, "thumbnail": 61, "tiff_read": 44, "tile": [2, 54, 58, 61, 71, 73], "tiledict": 11, "tileiter": 11, "tilesourc": 11, "type": 60, "upgrad": 74, "us": [70, 71], "usag": 66, "util": [7, 11], "valu": 54, "vector": 54, "version": 74, "view": 71, "within": 57, "write": [61, 71], "yaml": [62, 64], "z": 68, "zarr": 71}}) \ No newline at end of file diff --git a/tilesource_options.html b/tilesource_options.html new file mode 100644 index 000000000..201a68bc8 --- /dev/null +++ b/tilesource_options.html @@ -0,0 +1,275 @@ + + + + + + + Tile Source Options — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Tile Source Options🔗

+

Each tile source can have custom options that affect how tiles are generated from that tile source. All tile sources have a basic set of options:

+
+

Format🔗

+

Python tile functions can return tile data as images, numpy arrays, or PIL Image objects. The format parameter is one of the TILE_FORMAT_* constants. Refer to constants.py to view the format options.

+
+
+

Encoding🔗

+

The encoding parameter only affects output when format is TILE_FORMAT_IMAGE.

+

The encoding parameter can be one of JPEG, PNG, TIFF, JFIF, or TILED. When the tile is output as an image, this is the preferred format. Note that JFIF is a specific variant of JPEG that will always use either the Y or YCbCr color space as well as constraining other options. TILED will output a tiled tiff file; this is slower than TIFF but can support images of arbitrary size.

+

Additional encoding options are available based on the PIL.Image registered encoders. Refer to the PIL documentation for supported formats.

+

Associated with encoding, some image formats have additional parameters.

+
    +
  • JPEG and JFIF can specify jpegQuality, a number from 0 to 100 where 0 is small and 100 is higher-quality, and jpegSubsampling, where 0 is full chrominance data, 1 is half-resolution chrominance, and 2 is quarter-resolution chrominance.

  • +
  • TIFF can specify tiffCompression, which is one of the libtiff_ctypes.COMPRESSION* options found here.

  • +
+
+
+

Edges🔗

+

When a tile is requested at the right or bottom edge of the image, the tile could extend past the boundary of the image. If the image is not an even multiple of the tile size, the edge parameter determines how the tile is generated. A value of None (default) or False will generate a standard sized tile where the area outside of the image space could have pixels of any color. An edge value of 'crop' or True will return a tile that is smaller than the standard size. An edge value in the form of a hexadecimal encoded 8-bit-per-channel color (e.g., #rrggbb) will ensure that the area outside of the image space is all that color.

+
+
+

Style🔗

+

Often tiles are desired as 8-bit-per-sample images. However, if the tile source is more than 8 bits per sample or has more than 3 channels, some data will be lost. Similarly, if the data is returned as a numpy array, the range of values returned can vary by tile source. The style parameter can remap samples values and determine how channels are composited.

+

If style is {}, the default style for the file is used. If it is not specified or None, it will be the default style for non-geospatial tile sources and a default style consisting of the visible bands for geospatial sources. Otherwise, this is a json-encoded string that contains an object with a key of bands consisting of an array of band definitions. If only one band is needed, a json-encoded string of just the band definition can be used.

+

A band definition is an object which can contain the following keys:

+
    +
  • band: if -1 or None, the greyscale value is used. Otherwise, a 1-based numerical index into the channels of the image or a string that matches the interpretation of the band (‘red’, ‘green’, ‘blue’, ‘gray’, ‘alpha’). Note that ‘gray’ on an RGB or RGBA image will use the green band.

  • +
  • frame: if specified, override the frame parameter used in the tile query for this band. Note that it is more efficient to have at least one band not specify a frame parameter or use the same value as the basic query. Defaults to the frame value of the core query.

  • +
  • framedelta: if specified, and frame is not specified, override the frame parameter used in the tile query for this band by adding the value to the current frame number. If many different frames are being requested, all with the same framedelta, this is more efficient than varying the frame within the style.

  • +
  • min: the value to map to the first palette value. Defaults to 0. ‘auto’ to use 0 if the reported minimum and maximum of the band are between [0, 255] or use the reported minimum otherwise. ‘min’ or ‘max’ to always uses the reported minimum or maximum. ‘min:<threshold>’ and ‘max:<threshold>’ pick a value that excludes a threshold amount from the histogram; for instance, ‘min:0.02’ would exclude at most the dimmest 2% of values by using an appropriate value for the minimum based on a computed histogram with some default binning options. ‘auto:<threshold>’ works like auto, though it applies the threshold if the reported minimum would otherwise be used. ‘full’ is the same as specifying 0.

  • +
  • max: the value to map to the last palette value. Defaults to 255. ‘auto’ to use 0 if the reported minimum and maximum of the band are between [0, 255] or use the reported maximum otherwise. ‘min’ or ‘max’ to always uses the reported minimum or maximum. ‘min:<threshold>’ and ‘max:<threshold>’ pick a value that excludes a threshold amount from the histogram; for instance, ‘max:0.02’ would exclude at most the brightest 2% of values by using an appropriate value for the maximum based on a computed histogram with some default binning options. ‘auto:<threshold>’ works like auto, though it applies the threshold if the reported maximum would otherwise be used. ‘full’ uses a value based on the data type of the band. This will be 1 for a float data type, 255 for a uint8 data type, and 65535 for a uint16 data type.

  • +
  • palette: This is a single color string, a palette name, or a list of two or more colors. The values between min and max are interpolated using a piecewise linear algorithm or a nearest value algorithm (depending on the scheme) to map to the specified palette values. It can be specified in a variety of ways:

    +
      +
    • a list of two or more color values, where the color values are css-style strings (e.g., of the form #RRGGBB, #RRGGBBAA, #RGB, #RGBA, or a css rgb, rgba, hsl, or hsv string, or a css color name), or, if matplotlib is available, a matplotlib color name, or a list or tuple of RGB(A) values on a scale of [0-1].

    • +
    • a single string that is a color string as above. This is functionally a two-color palette with the first color as solid black (#000), and the second color the specified value

    • +
    • a named color palette from the palettable library (e.g., matplotlib.Plasma_6) or, if available, from the matplotlib library or one of its plugins (e.g., viridis).

    • +
    +
  • +
  • scheme: This is either linear (the default) or discrete. If a palette is specified, linear uses a piecewise linear interpolation, and discrete uses exact colors from the palette with the range of the data mapped into the specified number of colors (e.g., a palette with two colors will split exactly halfway between the min and max values).

  • +
  • nodata: the value to use for missing data. null or unset to not use a nodata value.

  • +
  • composite: either ‘lighten’ or ‘multiply’. Defaults to ‘lighten’ for all except the alpha band. Read more about blend modes and see examples here.

  • +
  • clamp: either True to clamp (also called clip or crop) values outside of the [min, max] to the ends of the palette or False to make outside values transparent.

  • +
  • dtype: if specified, cast the intermediate results to this data type. Only the first such value is used, and this can be specified as a base key if bands is specified. Normally, if a style is applied, the intermediate data is a numpy float array with values from [0,255]. If this is uint16, the results are multiplied by 65535 / 255 and cast to that dtype. If float, the results are divided by 255. If source, this uses the dtype of the source image.

  • +
  • axis: if specified, keep on the specified axis (channel) of the intermediate numpy array. This is typically between 0 and 3 for the red, green, blue, and alpha channels. Only the first such value is used, and this can be specified as a base key if bands is specified.

  • +
  • icc: by default, sources that expose ICC color profiles (PIL, OpenJPEG, OpenSlide, OMETiff, TIFF, TiffFile) will apply those profiles to the image data, converting the results to the sRGB profile. To use the raw image data without ICC profile adjustments, specify an icc value of false. If the entire style is {"icc": false}, the results will be the same as the default bands with only the adjustment being skipped. Similarly, if the entire style is {"icc": true}, this is the same as the default style with where the adjustment is applied. Besides a boolean, this may also be a string with one of the intents defined by the PIL.ImageCms.Intents enum. true is the same as perceptual. Note that not all tile sources expose ICC color profile information, even if the base file format contains it.

  • +
  • function: if specified, call a function to modify the resulting image. This can be specified as a base key and as a band key. Style functions can be called at multiple stages in the styling pipeline:

    +
      +
    • pre stage: this passes the original tile image to the function before any band data is applied.

    • +
    • preband stage: this passes the band image (often the original tile image if a different frame is not specified) to the function before any scaling.

    • +
    • band stage: this passes the band image after scaling (via min and max) and generating a nodata mask.

    • +
    • postband stage: this passes the in-progress output image after the band has been applied to it.

    • +
    • main stage: this passes the in-progress output image after all bands have been applied but before it is adjusted for dtype.

    • +
    • post stage: this passes the output image just before the style function returns.

    • +
    +

    The function parameter can be a single function or a list of functions. Items in a list of functions can, themselves, be lists of functions. A single function can be an object or a string. If a string, this is shorthand for {"name": <function>}. The function object contains (all but name are optional):

    +
      +
    • name: The name of a Python module and function that is installed in the same environment as large_image. For instance, large_image.tilesource.stylefuncs.maskPixelValues will use the function maskPixelValues in the large_image.tilesource.stylefuncs module. The function must be a Python function that takes a numpy array as the first parameter (the image) and has named parameters or kwargs for any passed parameters and possibly the style context.

    • +
    • parameters: A dictionary of parameters to pass to the function.

    • +
    • stage: A string for a single matching stage or a list of stages that this function should be applied to. This defaults to ["band", "main"].

    • +
    • context: If this is present and not falsy, pass the style context to the function. If this is true, the style context is passed as the context parameter. Otherwise, this is the name of the parameter that is passed to the function. The style context is a namespace that contains (depending on stage), a variety of information:

      +
        +
      • image: the source image as a numpy array.

      • +
      • originalStyle: the style object from the tile source.

      • +
      • style: the normalized style object (always an object with a bands key containing a list of bands).

      • +
      • x, y, z, and frame: the tile position in the source.

      • +
      • dtype, axis: the value specified from the style for these parameters.

      • +
      • output: the output image as a numpy array.

      • +
      • stage: the current stage of style processing.

      • +
      • styleIndex: if in a band stage, the 0-based index within the style bands.

      • +
      • band: the band numpy image in a band stage.

      • +
      • mask: a mask numpy image to use when applying the band.

      • +
      • palette: the normalized palette for a band.

      • +
      • palettebase: a numpy linear interpolation array for non-discrete palettes.

      • +
      • discrete: True if the scheme is discrete.

      • +
      • nodata: the nodata value for the band or None.

      • +
      • min, max: the resolved numerical minimum and maximum value for the band.

      • +
      • clamp: the clamp value for the band.

      • +
      +
    • +
    +
  • +
+

Note that some tile sources add additional options to the style parameter.

+
+

Examples🔗

+
+

Swap the red and green channels of a three color image🔗

+
style = {"bands": [
+  {"band": 1, "palette": ["#000", "#0f0"]},
+  {"band": 2, "palette": ["#000", "#f00"]},
+  {"band": 3, "palette": ["#000", "#00f"]}
+]}
+
+
+
+
+

Apply a gamma correction to the image🔗

+

This used a precomputed sixteen entry greyscale palette, computed as (value / 255) ** gamma * 255, where value is one of [0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255] and gamma is 0.5.

+
style = {"palette": [
+  "#000000", "#414141", "#5D5D5D", "#727272",
+  "#838383", "#939393", "#A1A1A1", "#AEAEAE",
+  "#BABABA", "#C5C5C5", "#D0D0D0", "#DADADA",
+  "#E4E4E4", "#EDEDED", "#F6F6F6", "#FFFFFF"
+]}
+
+
+
+
+

Composite several frames with framedelta🔗

+
style = {
+  "bands": [
+    {"framedelta": 0, "palette": "#0000FF"}
+    {"framedelta": 1, "palette": "#00FF00"}
+    {"framedelta": 2, "palette": "#FF0000"}
+  ],
+  "composite": "multiply"
+}
+
+
+
+
+

Fill missing data and apply categorical colormap🔗

+
style = {
+  "nodata": 0,
+  "min": 0,
+  "max": 6,
+  "clamp": "true",
+  "dtype": "uint8",
+  "scheme": "discrete",
+  "palette": ["#000000", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF"]
+}
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/upgrade.html b/upgrade.html new file mode 100644 index 000000000..bcd93ffa4 --- /dev/null +++ b/upgrade.html @@ -0,0 +1,150 @@ + + + + + + + Upgrading from Previous Versions — large_image documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Upgrading from Previous Versions🔗

+
+

Migration from Girder 2 to Girder 3🔗

+

If you are migrating a Girder 2 instance with Large Image to Girder 3, you need to do a one time database update. Specifically, one of the tile sources’ internal name changed.

+

Access the Girder Mongo database. The command for this in a simple installation is:

+
mongo girder
+
+
+

Update the tile source name by issuing the Mongo command:

+
db.item.updateMany({"largeImage.sourceName": "svs"}, {$set: {"largeImage.sourceName": "openslide"}})
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file