diff --git a/server/app/app.py b/server/app/app.py index 94a62a1d..2e3e418a 100644 --- a/server/app/app.py +++ b/server/app/app.py @@ -190,6 +190,11 @@ class SummarizeVarAPI(Resource): def post(self, data_adaptor): return common_rest.summarize_var_post(request, data_adaptor) +class SpatialImageAPI(Resource): + @rest_get_data_adaptor + def get(self, data_adaptor): + return common_rest.spatial_image_get(request, data_adaptor) + def get_api_base_resources(bp_base): """Add resources that are accessed from the api url""" @@ -222,6 +227,8 @@ def get_api_dataroot_resources(bp_dataroot): # Computation routes add_resource(DiffExpObsAPI, "/diffexp/obs") add_resource(LayoutObsAPI, "/layout/obs") + # Spatial routes + add_resource(SpatialImageAPI, "/spatial/image") return api diff --git a/server/common/rest.py b/server/common/rest.py index e7ff4114..d528fd73 100644 --- a/server/common/rest.py +++ b/server/common/rest.py @@ -5,7 +5,7 @@ from http import HTTPStatus import zlib import json -from flask import make_response, jsonify, current_app, abort +from flask import make_response, jsonify, current_app, abort, send_file from werkzeug.urls import url_unquote from server.common.config.client_config import get_client_config @@ -397,3 +397,39 @@ def summarize_var_post(request, data_adaptor): key = request.args.get("key", default=None) return summarize_var_helper(request, data_adaptor, key, request.get_data()) + +def spatial_image_get(request, data_adaptor): + import io + import matplotlib.pyplot + + resolution = "hires" + spatial = data_adaptor.get_spatial() + + if len(list(spatial)) == 0: + return abort_and_log(HTTPStatus.BAD_REQUEST, "uns does not have spatial information") + + library_id = list(spatial)[0] + if len(spatial) > 1: + current_app.logger.warning(f"More than one library found under uns.spatial, using library '{library_id}'") + + if "images" not in spatial[library_id]: + return abort_and_log(HTTPStatus.BAD_REQUEST, "spatial information does not contain images") + + if resolution not in spatial[library_id]["images"]: + return abort_and_log(HTTPStatus.BAD_REQUEST, f"spatial information does not contain requested resolution '{resolution}'") + + response_image = io.BytesIO() + matplotlib.pyplot.imsave(response_image, spatial[library_id]["images"][resolution]) + response_image.seek(0) + + try: + return send_file(response_image, attachment_filename=f"{library_id}-{resolution}.png", mimetype="image/png") + except (KeyError, DatasetAccessError) as e: + return abort_and_log(HTTPStatus.BAD_REQUEST, str(e), include_exc_info=True) + except PrepareError: + return abort_and_log( + HTTPStatus.NOT_IMPLEMENTED, + f"No spatial image available {request.path}", + loglevel=logging.ERROR, + include_exc_info=True, + ) diff --git a/server/data_anndata/anndata_adaptor.py b/server/data_anndata/anndata_adaptor.py index de0361ec..46f66412 100644 --- a/server/data_anndata/anndata_adaptor.py +++ b/server/data_anndata/anndata_adaptor.py @@ -274,6 +274,9 @@ class AnndataAdaptor(DataAdaptor): df = df[fields] return encode_matrix_fbs(df, col_idx=df.columns) + def get_spatial(self): + return self.data.uns["spatial"] + def get_embedding_names(self): """ Return pre-computed embeddings. diff --git a/server/requirements.txt b/server/requirements.txt index fb78f26c..64b1895d 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -14,6 +14,7 @@ flatten-dict>=0.2.0 fsspec>=0.4.4,<0.8.0 gunicorn>=20.0.4 h5py>=3.0.0 +matplotlib>=3.5.0 numba>=0.51.2 numpy>=1.17.5 packaging>=20.0