Source code for tiatoolbox.visualization.tileserver

"""Simple Flask WSGI apps to display tiles as slippery maps."""
from __future__ import annotations

import io
import json
from pathlib import Path
from typing import Dict, List, Union

import numpy as np
from flask import Flask, Response, send_file
from flask.templating import render_template
from PIL import Image

from tiatoolbox import data
from tiatoolbox.annotation.storage import SQLiteStore
from tiatoolbox.tools.pyramid import AnnotationTileGenerator, ZoomifyGenerator
from tiatoolbox.utils.visualization import AnnotationRenderer, colourise_image
from tiatoolbox.wsicore.wsireader import VirtualWSIReader, WSIReader


[docs]class TileServer(Flask): """A Flask app to display Zoomify tiles as a slippery map. Args: title (str): The title of the tile server, displayed in the browser as the page title. layers (Dict[str, WSIReader | str] | List[WSIReader | str]): A dictionary mapping layer names to image paths or :obj:`WSIReader` objects to display. May also be a list, in which case generic names 'layer-1', 'layer-2' etc. will be used. If layer is a single-channel low-res overlay, it will be colourized using the 'viridis' colourmap Examples: >>> from tiatoolbox.wsicore.wsireader import WSIReader >>> from tiatoolbox.visualization.tileserver import TileServer >>> wsi = WSIReader.open("CMU-1.svs") >>> app = TileServer( ... title="Testing TileServer", ... layers={ ... "My SVS": wsi, ... }, ... ) >>> app.run() """ def __init__( self, title: str, layers: Union[Dict[str, Union[WSIReader, str]], List[Union[WSIReader, str]]], renderer: AnnotationRenderer = None, ) -> None: super().__init__( __name__, template_folder=data._local_sample_path( Path("visualization") / "templates" ), static_url_path="", static_folder=data._local_sample_path(Path("visualization") / "static"), ) self.tia_title = title self.tia_layers = {} self.tia_pyramids = {} self.renderer = renderer # only used if a layer is rendering form a store. # Generic layer names if none provided. if isinstance(layers, list): layers = {f"layer-{i}": p for i, p in enumerate(layers)} # Set up the layer dict. meta = None for i, (key, layer) in enumerate(layers.items()): layer = self._get_layer_as_wsireader(layer, meta) self.tia_layers[key] = layer if isinstance(layer, WSIReader): self.tia_pyramids[key] = ZoomifyGenerator(layer) else: self.tia_pyramids[key] = layer # it's an AnnotationTileGenerator if i == 0: meta = layer.info # base slide info self.route( "/layer/<layer>/zoomify/TileGroup<int:tile_group>/" "<int:z>-<int:x>-<int:y>.jpg" )( self.zoomify, ) self.route("/")(self.index) def _get_layer_as_wsireader(self, layer, meta): """Gets appropriate image provider for layer. Args: layer (str | ndarray | WSIReader): A reference to an image or annotations to be displayed. meta (WSIMeta): The metadata of the base slide. Returns: WSIReader or AnnotationTileGenerator: The appropriate image source for the layer. """ if isinstance(layer, (str, Path)): layer_path = Path(layer) if layer_path.suffix in [".jpg", ".png"]: # Assume it's a low-res heatmap. layer = np.array(Image.open(layer_path)) elif layer_path.suffix == ".db": # Assume it's an annotation store. layer = AnnotationTileGenerator( meta, SQLiteStore(layer_path), self.renderer ) elif layer_path.suffix == ".geojson": # Assume annotations in geojson format layer = AnnotationTileGenerator( meta, SQLiteStore.from_geojson(layer_path), self.renderer, ) else: # Assume it's a WSI. return WSIReader.open(layer_path) if isinstance(layer, np.ndarray): # Make into rgb if single channel. layer = colourise_image(layer) return VirtualWSIReader(layer, info=meta) return layer
[docs] def zoomify( self, layer: str, tile_group: int, z: int, x: int, y: int # skipcq: PYL-w0613 ) -> Response: """Serve a Zoomify tile for a particular layer. Note that this should not be called directly, but will be called automatically by the Flask framework when a client requests a tile at the registered URL. Args: layer (str): The layer name. tile_group (int): The tile group. Currently unused. z (int): The zoom level. x (int): The x coordinate. y (int): The y coordinate. Returns: flask.Response: The tile image response. """ try: pyramid = self.tia_pyramids[layer] except KeyError: return Response("Layer not found", status=404) try: tile_image = pyramid.get_tile(level=z, x=x, y=y) except IndexError: return Response("Tile not found", status=404) image_io = io.BytesIO() tile_image.save(image_io, format="webp") image_io.seek(0) return send_file(image_io, mimetype="image/webp")
[docs] def index(self) -> Response: """Serve the index page. Returns: flask.Response: The index page. """ layers = [ { "name": name, "url": f"/layer/{name}/zoomify/{{TileGroup}}/{{z}}-{{x}}-{{y}}.jpg", "size": [int(x) for x in layer.info.slide_dimensions], "mpp": float(np.mean(layer.info.mpp)), } for name, layer in self.tia_layers.items() ] return render_template( "index.html", title=self.tia_title, layers=json.dumps(layers) )