Skip to content

Commit

Permalink
Initial reversion
Browse files Browse the repository at this point in the history
  • Loading branch information
sectra-masve committed Jan 5, 2024
1 parent 0f60a06 commit 1c1bd79
Show file tree
Hide file tree
Showing 14 changed files with 710 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
# Sectra Digital Pathology (DPAT) ImageAnalysis (IA) API - Software Development Kit (SDK)

## About
This is a work in progress repository providing examples on how to integrate with the Sectra IA-API.

Currently only a basic application exist that returns (random) results given user input.

See the `examples` folder for more info. A roadmap will be provided soon.

## Changelog

### 2024-01-05

- Added basic example `examples/python/ia_app_basic/` showing how an app can accept user input and return results for a specific sub-area of a WSI.
32 changes: 32 additions & 0 deletions examples/python/ia_app_basic/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Temporary Python files
*.pyc
*.egg-info
__pycache__
.ipynb_checkpoints

# Temporary OS files
Icon*

# Temporary virtual environment files
/.cache/
/.venv/

# Temporary server files
.env
*.pid

# Build and release directories
/build/
/dist/
/debug/
*.spec

# Sublime Text
*.sublime-workspace

# Eclipse
.settings

# Don't include
scripts
.python-version
58 changes: 58 additions & 0 deletions examples/python/ia_app_basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
pcaddemo
========

## About
This is a demo repository for a simple Sectra Image Analysis API (IA-API) implementation.

Author: Martin Lindvall, [email protected]

## Application function

- Input: User draws a polygon around a region of interest ( taggedPolygon, see `webserver.py:app_return_registerinfo` )
- Output: a graphical primitive, or a "patch gallery" (see `webserver.py:DEMO_TYPE` and `webserver.py:app_on_userinput` )

## Development notes
Uses a simple flask server for responding to IA-API requests and a requests-based client for outgoing communications with the IA-API..

For simplicity, uses no async libraries. For load-scaling, simply run with number of workers processes corresponding to expected max number of simultaneous users. Tools such as gunicorn or a similar pre-forking worker server is recommended to spawn the workers.

Should have no trouble scaling as long as number of max simultaneous users are below 1000 or so.

### Install and run

You can run this example in a virtualenv using poetry, or directly in the current python environment.

Using poetry:
```
poetry run python pcaddemo/__main__.py
```

As plain-old python:

```
python setup.py develop
python pcaddemo/__main__.py
```

## Sectra Server configuration

Running this demo starts a web server on 0.0.0.0 (all local ips) listening on port 5001.

You need to configure the Sectra Pathology Server (SPS) to call this server. This involves:

1. Ensuring that the SPS can reach this host over the network (ensure there is a route and that firewall rules allow it)
2. Registering the IA-API app in the configuration interface (see chapter 7 in the System Administrator Manual)
- in brief: goto https://<pathologyserver>/sectrapathologyimport/config
- log in, click Image Analysis Applications
- Under Server Side Applications, click 'register new'
- Enter the URL where your started web server (this repository) is running, as reachable from the pathology server. Example: `http://my-ia-app-server:5001/iademo`
- Press 'Retrieve registration Info'
- The fields should be populated, press *Save*
- Per default, the app is disabled. Click the 'disabled' button to toggle it to enabled.

If succesful, you should now be able to right-click in any Pathology Image and select your new IA-APP (you might need to refresh any running sessions).


## Tested with

- DPAT 3.4 on 2024-01-05
4 changes: 4 additions & 0 deletions examples/python/ia_app_basic/flask_run.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
set FLASK_APP=pcaddemo\webserver
set FLASK_RUN_PORT=5005
set FLASK_DEBUG=1
flask run
6 changes: 6 additions & 0 deletions examples/python/ia_app_basic/flask_run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
export FLASK_APP=pcaddemo/webserver
export FLASK_RUN_PORT=5005
export FLASK_RUN_HOST=0.0.0.0
export FLASK_DEBUG=1
flask run
3 changes: 3 additions & 0 deletions examples/python/ia_app_basic/pcaddemo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
""" pcad demo """

from .version import __version__
6 changes: 6 additions & 0 deletions examples/python/ia_app_basic/pcaddemo/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env python

if __name__ == "__main__":
from pcaddemo.webserver import app, BIND_PORT

app.run(host="0.0.0.0", port=BIND_PORT, debug=False)
83 changes: 83 additions & 0 deletions examples/python/ia_app_basic/pcaddemo/analysisapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-

"""
analysisapi -- python client for the Sectra Pathology Image Analysis API
This is a requests-style, non-async implementation.
"""

import logging as log
from io import BytesIO
import time
import requests

DEFAULT_TIMEOUT = (
3.0 * 2 + 0.05,
30.0 * 2,
) # ( connect_timeout, read_timeout ) in seconds


class AnalysisApi:
def __init__(self, url, hookname, token=None, api_version="1.5"):
"""
@param url -- typically https://localhost/SectraPathologyServer/external/imageanalysis/v1
"""
self.url = url
self.name = hookname
self.token = token
self._session = requests.Session()
self.api_version = api_version

def _auth_get(self, url, ok_codes=[200]):
"""make GET request adding the bearer token"""
r = self._session.get(url, headers=self._headers(), timeout=DEFAULT_TIMEOUT)
if not r.status_code in ok_codes:
r.raise_for_status()
return r

def slideinfo(self, slide_id):
"""GET slide metadata for slide"""
r = self._auth_get("{}/slides/{}/info".format(self.url, slide_id))
slide_info = r.json()["blocks"][0]["slides"][0] # yeah, weird
return slide_info

def tile(self, slide_id, dzilvl, col, row, zstack=0, format="jpg"):
"""retrieve an image tile"""
tile_url = "{}/images/{}_files/{}/{}_{}_{}.{}".format(
self.url, slide_id, dzilvl, col, row, zstack, format
)
r = self._session.get(
tile_url, headers=self._headers(), timeout=DEFAULT_TIMEOUT
)
if not r.status_code == 200:
r.raise_for_status()
return BytesIO(r.content)

def store_result(self, result_data):
"""store result into sectra pathology db"""
url = f"{self.url}/hooks/{self.name}/results/"
r = self._session.post(
url, headers=self._headers(), timeout=DEFAULT_TIMEOUT, json=result_data
)
if not r.status_code == 200:
r.raise_for_status()
parsed = r.json()
return parsed

def update_result(self, result_data):
"""update existing result"""
result_id = result_data["id"]
url = f"{self.url}/hooks/{self.name}/results/{result_id}"
r = self._session.put(
url, headers=self._headers(), timeout=DEFAULT_TIMEOUT, json=result_data
)
if not r.status_code == 200:
r.raise_for_status()
parsed = r.json()
return parsed

def _headers(self):
return {
"Authorization": "Bearer {}".format(self.token),
"X-Sectra-ApiVersion": f"{self.api_version}",
}
16 changes: 16 additions & 0 deletions examples/python/ia_app_basic/pcaddemo/geometry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import json
import random

from shapely.geometry import Polygon, Point


def sectra_polygon_to_shapely(data_polygon):
return Polygon([(pt["x"], pt["y"]) for pt in data_polygon["points"]])


def random_point_in_polygon(poly):
minx, miny, maxx, maxy = poly.bounds
while True:
p = Point(random.uniform(minx, maxx), random.uniform(miny, maxy))
if poly.contains(p):
return p
23 changes: 23 additions & 0 deletions examples/python/ia_app_basic/pcaddemo/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""
version info
"""
__version__ = "0.1.0"

# this is the minimum version we require, we do not support
# servers that run earlier versions.
#
# Bump this number when you add expectations on API datastructures or endpoints only
# present from version X.Y onward.
#
# 1.5 here corresponds to DPAT 3.0 and we make use of /slides/<id>/info endpoint
# only available from this version and onward
SECTRA_IA_API_MIN_VERSION = "1.5"

# this is the highest version we've explicitly tested with
# higher-capable servers should fallback to this api version to ensure compatability
#
# Bump this to the highest number you've tested with.
# If you are lazy, set:
# SECTRA_IA_API_MAX_VERSION = SECTRA_IA_API_MIN_VERSION
# (you might lose some enhancements that you do not rely on anyway)
SECTRA_IA_API_MAX_VERSION = "1.8"
Loading

0 comments on commit 1c1bd79

Please sign in to comment.