imaging-server-kit 0.0.5__tar.gz → 0.0.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/Dockerfile +1 -2
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/Dockerfile-GPU +1 -1
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/PKG-INFO +10 -7
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/README.md +5 -5
- imaging_server_kit-0.0.6/main.py +10 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/pyproject.toml +2 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/__init__.py +8 -2
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/_version.py +2 -2
- imaging_server_kit-0.0.5/src/imaging_server_kit/registry.py → imaging_server_kit-0.0.6/src/imaging_server_kit/algorithm_hub.py +1 -1
- imaging_server_kit-0.0.6/src/imaging_server_kit/authenticated_server.py +249 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/client.py +30 -4
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/geometry.py +75 -25
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/serialization.py +4 -4
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/server.py +31 -15
- imaging_server_kit-0.0.6/src/imaging_server_kit/static/js/login_script.js +24 -0
- imaging_server_kit-0.0.6/src/imaging_server_kit/static/js/register_script.js +15 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/templates/index.html +1 -1
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/templates/info.html +1 -1
- imaging_server_kit-0.0.6/src/imaging_server_kit/templates/login.html +27 -0
- imaging_server_kit-0.0.6/src/imaging_server_kit/templates/register.html +27 -0
- imaging_server_kit-0.0.6/src/imaging_server_kit/users_utils/__init__.py +3 -0
- imaging_server_kit-0.0.6/src/imaging_server_kit/users_utils/db.py +34 -0
- imaging_server_kit-0.0.6/src/imaging_server_kit/users_utils/schemas.py +15 -0
- imaging_server_kit-0.0.6/src/imaging_server_kit/users_utils/users.py +55 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/web_demo.py +6 -6
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/PKG-INFO +10 -7
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/SOURCES.txt +14 -5
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/requires.txt +2 -0
- imaging_server_kit-0.0.6/start_algorithm_hub.py +8 -0
- imaging_server_kit-0.0.5/main.py +0 -8
- imaging_server_kit-0.0.5/start_registry.py +0 -8
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/.dockerignore +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/.gitattributes +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/.github/workflows/publish.yml +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/.gitignore +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/LICENSE +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/MANIFEST.in +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/build.sh +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/examples/.gitkeep +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/setup.cfg +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/assets/style.css +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/encoding.py +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/errors.py +0 -0
- {imaging_server_kit-0.0.5/src/imaging_server_kit/static → imaging_server_kit-0.0.6/src/imaging_server_kit/static/css}/index.css +0 -0
- {imaging_server_kit-0.0.5/src/imaging_server_kit/static → imaging_server_kit-0.0.6/src/imaging_server_kit/static/css}/info.css +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/dependency_links.txt +0 -0
- {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: imaging-server-kit
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Deploy image processing algorithms in FastAPI servers.
|
|
5
5
|
Author-email: Mallory Wittwer <mallory.wittwer@epfl.ch>
|
|
6
6
|
License: Copyright (c) 2024, EPFL
|
|
@@ -50,6 +50,8 @@ License-File: LICENSE
|
|
|
50
50
|
Requires-Dist: fastapi
|
|
51
51
|
Requires-Dist: uvicorn[standard]
|
|
52
52
|
Requires-Dist: python-multipart
|
|
53
|
+
Requires-Dist: fastapi-users[sqlalchemy]
|
|
54
|
+
Requires-Dist: aiosqlite
|
|
53
55
|
Requires-Dist: tifffile
|
|
54
56
|
Requires-Dist: numpy
|
|
55
57
|
Requires-Dist: scikit-image
|
|
@@ -62,6 +64,7 @@ Requires-Dist: dash
|
|
|
62
64
|
Requires-Dist: dash-bootstrap-components
|
|
63
65
|
Requires-Dist: a2wsgi
|
|
64
66
|
Requires-Dist: pytest
|
|
67
|
+
Dynamic: license-file
|
|
65
68
|
|
|
66
69
|

|
|
67
70
|
# 🪐 Imaging Server Kit
|
|
@@ -91,19 +94,19 @@ All of these algorithm servers can be installed locally or built and run with do
|
|
|
91
94
|
|
|
92
95
|
```{docker-compose.yml}
|
|
93
96
|
services:
|
|
94
|
-
|
|
97
|
+
algorithm_hub:
|
|
95
98
|
image: mallorywittwerepfl/imaging-server-kit:latest
|
|
96
99
|
ports:
|
|
97
100
|
- "8000:8000"
|
|
98
|
-
command: python3
|
|
101
|
+
command: python3 start_algorithm_hub.py
|
|
99
102
|
rembg:
|
|
100
103
|
image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-rembg:latest
|
|
101
104
|
depends_on:
|
|
102
|
-
-
|
|
105
|
+
- algorithm_hub
|
|
103
106
|
skimage-log:
|
|
104
107
|
image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-skimage-log:latest
|
|
105
108
|
depends_on:
|
|
106
|
-
-
|
|
109
|
+
- algorithm_hub
|
|
107
110
|
```
|
|
108
111
|
|
|
109
112
|
Start the server with
|
|
@@ -173,7 +176,7 @@ Build the `imaging-server-kit` docker image for a specific Python version:
|
|
|
173
176
|
docker build --build-arg PYTHON_VERSION=3.9 -t imaging-server-kit:3.9 .
|
|
174
177
|
```
|
|
175
178
|
|
|
176
|
-
Run [build.sh](./build.sh) to build docker images for Python 3.9, 3.10, 3.11, GPU and the
|
|
179
|
+
Run [build.sh](./build.sh) to build docker images for Python 3.9, 3.10, 3.11, GPU and the algorithm hub:
|
|
177
180
|
|
|
178
181
|
```
|
|
179
182
|
bash ./build.sh
|
|
@@ -26,19 +26,19 @@ All of these algorithm servers can be installed locally or built and run with do
|
|
|
26
26
|
|
|
27
27
|
```{docker-compose.yml}
|
|
28
28
|
services:
|
|
29
|
-
|
|
29
|
+
algorithm_hub:
|
|
30
30
|
image: mallorywittwerepfl/imaging-server-kit:latest
|
|
31
31
|
ports:
|
|
32
32
|
- "8000:8000"
|
|
33
|
-
command: python3
|
|
33
|
+
command: python3 start_algorithm_hub.py
|
|
34
34
|
rembg:
|
|
35
35
|
image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-rembg:latest
|
|
36
36
|
depends_on:
|
|
37
|
-
-
|
|
37
|
+
- algorithm_hub
|
|
38
38
|
skimage-log:
|
|
39
39
|
image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-skimage-log:latest
|
|
40
40
|
depends_on:
|
|
41
|
-
-
|
|
41
|
+
- algorithm_hub
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Start the server with
|
|
@@ -108,7 +108,7 @@ Build the `imaging-server-kit` docker image for a specific Python version:
|
|
|
108
108
|
docker build --build-arg PYTHON_VERSION=3.9 -t imaging-server-kit:3.9 .
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
-
Run [build.sh](./build.sh) to build docker images for Python 3.9, 3.10, 3.11, GPU and the
|
|
111
|
+
Run [build.sh](./build.sh) to build docker images for Python 3.9, 3.10, 3.11, GPU and the algorithm hub:
|
|
112
112
|
|
|
113
113
|
```
|
|
114
114
|
bash ./build.sh
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import uvicorn
|
|
2
|
+
import imaging_server_kit as serverkit
|
|
3
|
+
|
|
4
|
+
server = serverkit.AlgorithmServer(
|
|
5
|
+
algorithm_name="foo", parameters_model=serverkit.Parameters
|
|
6
|
+
)
|
|
7
|
+
app = server.app
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
uvicorn.run("main:app", host="0.0.0.0", port=8000)
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
from ._version import version as __version__
|
|
2
|
+
|
|
2
3
|
try:
|
|
3
4
|
from ._version import version as __version__
|
|
4
5
|
except ImportError:
|
|
5
6
|
__version__ = "unknown"
|
|
6
7
|
|
|
7
8
|
from imaging_server_kit.client import Client
|
|
8
|
-
from imaging_server_kit.server import
|
|
9
|
-
from imaging_server_kit.
|
|
9
|
+
from imaging_server_kit.server import AlgorithmServer, Parameters
|
|
10
|
+
from imaging_server_kit.authenticated_server import AuthenticatedAlgorithmServer
|
|
11
|
+
from imaging_server_kit.algorithm_hub import AlgorithmHub
|
|
10
12
|
from imaging_server_kit.encoding import encode_contents, decode_contents
|
|
11
13
|
from imaging_server_kit.serialization import (
|
|
12
14
|
serialize_result_tuple,
|
|
@@ -17,6 +19,10 @@ from imaging_server_kit.geometry import (
|
|
|
17
19
|
instance_mask2features,
|
|
18
20
|
features2mask,
|
|
19
21
|
features2instance_mask,
|
|
22
|
+
features2mask_3d,
|
|
23
|
+
mask2features_3d,
|
|
24
|
+
instance_mask2features_3d,
|
|
25
|
+
features2instance_mask_3d,
|
|
20
26
|
boxes2features,
|
|
21
27
|
features2boxes,
|
|
22
28
|
points2features,
|
|
@@ -11,7 +11,7 @@ static_dir = importlib.resources.files("imaging_server_kit").joinpath("static")
|
|
|
11
11
|
templates = Jinja2Templates(directory=str(templates_dir))
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class
|
|
14
|
+
class AlgorithmHub:
|
|
15
15
|
def __init__(self) -> None:
|
|
16
16
|
self.app = FastAPI()
|
|
17
17
|
self.app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import importlib.resources
|
|
2
|
+
import os
|
|
3
|
+
from typing import List, Tuple, Type
|
|
4
|
+
|
|
5
|
+
import imaging_server_kit as serverkit
|
|
6
|
+
import requests
|
|
7
|
+
import yaml
|
|
8
|
+
from fastapi import FastAPI, Request, status, HTTPException, Depends
|
|
9
|
+
from fastapi.responses import HTMLResponse
|
|
10
|
+
from fastapi.staticfiles import StaticFiles
|
|
11
|
+
from fastapi.templating import Jinja2Templates
|
|
12
|
+
from imaging_server_kit.web_demo import generate_dash_app
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
from a2wsgi import WSGIMiddleware
|
|
15
|
+
from contextlib import asynccontextmanager
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
templates_dir = importlib.resources.files("imaging_server_kit").joinpath("templates")
|
|
19
|
+
static_dir = importlib.resources.files("imaging_server_kit").joinpath("static")
|
|
20
|
+
|
|
21
|
+
templates = Jinja2Templates(directory=str(templates_dir))
|
|
22
|
+
|
|
23
|
+
PROCESS_TIMEOUT_SEC = 3600
|
|
24
|
+
|
|
25
|
+
from imaging_server_kit.users_utils import (
|
|
26
|
+
create_db_and_tables,
|
|
27
|
+
fastapi_users,
|
|
28
|
+
auth_backend,
|
|
29
|
+
UserRead,
|
|
30
|
+
UserUpdate,
|
|
31
|
+
UserCreate,
|
|
32
|
+
User,
|
|
33
|
+
current_active_user,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_from_yaml(file_path: str):
|
|
38
|
+
with open(file_path, "r") as file:
|
|
39
|
+
data = yaml.safe_load(file)
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_algo_params_schema(algo_params_schema):
|
|
44
|
+
algo_params = algo_params_schema.get("properties")
|
|
45
|
+
required_params = algo_params_schema.get("required")
|
|
46
|
+
for param in algo_params.keys():
|
|
47
|
+
algo_params[param]["required"] = param in required_params
|
|
48
|
+
return algo_params
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
ALGORITHM_HUB_URL = os.getenv("ALGORITHM_HUB_URL", "http://algorithm_hub:8000")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Parameters(BaseModel):
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(extra="forbid")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AuthenticatedAlgorithmServer:
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
algorithm_name: str,
|
|
64
|
+
parameters_model: Type[BaseModel],
|
|
65
|
+
metadata_file: str = None,
|
|
66
|
+
):
|
|
67
|
+
self.algorithm_name = algorithm_name
|
|
68
|
+
self.parameters_model = parameters_model
|
|
69
|
+
|
|
70
|
+
if metadata_file is None:
|
|
71
|
+
self.metadata_file = "metadata.yaml"
|
|
72
|
+
else:
|
|
73
|
+
self.metadata_file = metadata_file
|
|
74
|
+
|
|
75
|
+
self.app = FastAPI(title=algorithm_name, lifespan=self.lifespan)
|
|
76
|
+
|
|
77
|
+
# Users
|
|
78
|
+
self.app.include_router(
|
|
79
|
+
fastapi_users.get_auth_router(auth_backend),
|
|
80
|
+
prefix="/auth/jwt",
|
|
81
|
+
tags=["auth"],
|
|
82
|
+
)
|
|
83
|
+
self.app.include_router(
|
|
84
|
+
fastapi_users.get_register_router(UserRead, UserCreate),
|
|
85
|
+
prefix="/auth",
|
|
86
|
+
tags=["auth"],
|
|
87
|
+
)
|
|
88
|
+
self.app.include_router(
|
|
89
|
+
fastapi_users.get_reset_password_router(),
|
|
90
|
+
prefix="/auth",
|
|
91
|
+
tags=["auth"],
|
|
92
|
+
)
|
|
93
|
+
self.app.include_router(
|
|
94
|
+
fastapi_users.get_verify_router(UserRead),
|
|
95
|
+
prefix="/auth",
|
|
96
|
+
tags=["auth"],
|
|
97
|
+
)
|
|
98
|
+
self.app.include_router(
|
|
99
|
+
fastapi_users.get_users_router(UserRead, UserUpdate),
|
|
100
|
+
prefix="/users",
|
|
101
|
+
tags=["users"],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Info HTML
|
|
105
|
+
self.app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
|
|
106
|
+
|
|
107
|
+
# Web demo app
|
|
108
|
+
algo_params = parameters_model.model_json_schema().get("properties")
|
|
109
|
+
|
|
110
|
+
dash_app = generate_dash_app(
|
|
111
|
+
algorithm_name,
|
|
112
|
+
algo_params,
|
|
113
|
+
run_fnct=self.run_algorithm,
|
|
114
|
+
sample_image_fnct=self.load_sample_images,
|
|
115
|
+
prefix=f"/{algorithm_name}/demo/",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.app.mount(f"/{algorithm_name}/demo", WSGIMiddleware(dash_app.server))
|
|
119
|
+
|
|
120
|
+
self.register_routes()
|
|
121
|
+
|
|
122
|
+
self.services = [self.algorithm_name]
|
|
123
|
+
|
|
124
|
+
@asynccontextmanager
|
|
125
|
+
async def lifespan(self, app: FastAPI):
|
|
126
|
+
await self.register_with_algohub()
|
|
127
|
+
await create_db_and_tables()
|
|
128
|
+
yield
|
|
129
|
+
await self.deregister_from_algohub()
|
|
130
|
+
|
|
131
|
+
async def register_with_algohub(self):
|
|
132
|
+
try:
|
|
133
|
+
response = requests.get(f"{ALGORITHM_HUB_URL}/")
|
|
134
|
+
except Exception:
|
|
135
|
+
print("Algorithm hub unavailable.")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
response = requests.post(
|
|
139
|
+
f"{ALGORITHM_HUB_URL}/register",
|
|
140
|
+
json={
|
|
141
|
+
"name": self.algorithm_name,
|
|
142
|
+
"url": f"http://{self.algorithm_name}:8000",
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
if response.status_code == 201:
|
|
146
|
+
print(f"Service {self.algorithm_name} registered successfully.")
|
|
147
|
+
else:
|
|
148
|
+
print(f"Failed to register {self.algorithm_name}: {response.json()}")
|
|
149
|
+
|
|
150
|
+
async def deregister_from_algohub(self):
|
|
151
|
+
deregister_url = f"{ALGORITHM_HUB_URL}/deregister"
|
|
152
|
+
response = requests.post(deregister_url, json={"name": self.algorithm_name})
|
|
153
|
+
if response.status_code == 201:
|
|
154
|
+
print(f"Service {self.algorithm_name} deregistered.")
|
|
155
|
+
else:
|
|
156
|
+
print(f"Failed to deregister {self.algorithm_name}: {response.json()}")
|
|
157
|
+
|
|
158
|
+
def register_routes(self):
|
|
159
|
+
@self.app.get("/")
|
|
160
|
+
def home():
|
|
161
|
+
return list_services()
|
|
162
|
+
|
|
163
|
+
@self.app.get("/services")
|
|
164
|
+
def list_services():
|
|
165
|
+
return {"services": self.services}
|
|
166
|
+
|
|
167
|
+
@self.app.get("/version")
|
|
168
|
+
def get_version():
|
|
169
|
+
return serverkit.__version__
|
|
170
|
+
|
|
171
|
+
@self.app.get(f"/{self.algorithm_name}/info", response_class=HTMLResponse)
|
|
172
|
+
def info(request: Request):
|
|
173
|
+
algo_info = load_from_yaml(self.metadata_file)
|
|
174
|
+
algo_params_schema = get_algo_params()
|
|
175
|
+
algo_params = parse_algo_params_schema(algo_params_schema)
|
|
176
|
+
return templates.TemplateResponse(
|
|
177
|
+
"info.html",
|
|
178
|
+
{
|
|
179
|
+
"request": request,
|
|
180
|
+
"algo_info": algo_info,
|
|
181
|
+
"algo_params": algo_params,
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
@self.app.get(f"/register", response_class=HTMLResponse)
|
|
186
|
+
def register(request: Request):
|
|
187
|
+
return templates.TemplateResponse("register.html", {"request": request})
|
|
188
|
+
|
|
189
|
+
@self.app.get(f"/login", response_class=HTMLResponse)
|
|
190
|
+
def login(request: Request):
|
|
191
|
+
return templates.TemplateResponse("login.html", {"request": request})
|
|
192
|
+
|
|
193
|
+
@self.app.post(
|
|
194
|
+
f"/{self.algorithm_name}/process", status_code=status.HTTP_201_CREATED
|
|
195
|
+
)
|
|
196
|
+
async def run_algo(
|
|
197
|
+
algo_params: self.parameters_model,
|
|
198
|
+
user: User = Depends(current_active_user),
|
|
199
|
+
):
|
|
200
|
+
try:
|
|
201
|
+
result_data_tuple = await asyncio.wait_for(
|
|
202
|
+
self._run_algorithm(**algo_params.dict()),
|
|
203
|
+
timeout=PROCESS_TIMEOUT_SEC,
|
|
204
|
+
)
|
|
205
|
+
except (
|
|
206
|
+
asyncio.TimeoutError
|
|
207
|
+
): # This works but doesn't actually kill the process... we need something else to terminate it
|
|
208
|
+
raise HTTPException(
|
|
209
|
+
status_code=504, detail="Request timeout during processing."
|
|
210
|
+
)
|
|
211
|
+
try:
|
|
212
|
+
serialized_results = await asyncio.wait_for(
|
|
213
|
+
self._serialize_result_tuple(result_data_tuple),
|
|
214
|
+
timeout=PROCESS_TIMEOUT_SEC,
|
|
215
|
+
)
|
|
216
|
+
except asyncio.TimeoutError:
|
|
217
|
+
raise HTTPException(
|
|
218
|
+
status_code=504, detail="Request timeout during serialization."
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
return serialized_results
|
|
222
|
+
|
|
223
|
+
@self.app.get(f"/{self.algorithm_name}/parameters", response_model=dict)
|
|
224
|
+
def get_algo_params():
|
|
225
|
+
return self.parameters_model.model_json_schema()
|
|
226
|
+
|
|
227
|
+
@self.app.get(f"/{self.algorithm_name}/sample_images", response_model=dict)
|
|
228
|
+
def get_sample_images():
|
|
229
|
+
images = self.load_sample_images()
|
|
230
|
+
encoded_images = [
|
|
231
|
+
{"sample_image": serverkit.encode_contents(image)} for image in images
|
|
232
|
+
]
|
|
233
|
+
return {"sample_images": encoded_images}
|
|
234
|
+
|
|
235
|
+
async def _serialize_result_tuple(self, result_data_tuple):
|
|
236
|
+
serialized_results = await asyncio.to_thread(
|
|
237
|
+
serverkit.serialize_result_tuple, result_data_tuple
|
|
238
|
+
)
|
|
239
|
+
return serialized_results
|
|
240
|
+
|
|
241
|
+
async def _run_algorithm(self, **algo_params):
|
|
242
|
+
result_data_tuple = await asyncio.to_thread(self.run_algorithm, **algo_params)
|
|
243
|
+
return result_data_tuple
|
|
244
|
+
|
|
245
|
+
def load_sample_images(self) -> List["np.ndarray"]:
|
|
246
|
+
raise NotImplementedError("Subclasses should implement this method")
|
|
247
|
+
|
|
248
|
+
def run_algorithm(self, **algo_params) -> List[Tuple]:
|
|
249
|
+
raise NotImplementedError("Subclasses should implement this method")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import requests
|
|
2
|
+
import webbrowser
|
|
2
3
|
from typing import List, Dict, Tuple
|
|
3
4
|
import numpy as np
|
|
4
5
|
import imaging_server_kit as serverkit
|
|
@@ -17,6 +18,8 @@ class Client:
|
|
|
17
18
|
self._algorithms = {}
|
|
18
19
|
if server_url != "":
|
|
19
20
|
self.connect(server_url)
|
|
21
|
+
|
|
22
|
+
self.token = None
|
|
20
23
|
|
|
21
24
|
def connect(self, server_url: str) -> None:
|
|
22
25
|
self.server_url = server_url
|
|
@@ -31,6 +34,31 @@ class Client:
|
|
|
31
34
|
self.algorithms = services
|
|
32
35
|
else:
|
|
33
36
|
raise AlgorithmServerError(response.status_code, response.text)
|
|
37
|
+
|
|
38
|
+
# def browser_login(self):
|
|
39
|
+
# webbrowser.open(f"{self.server_url}/login")
|
|
40
|
+
# # How to retreive the access_token when the user logs in via the web page?
|
|
41
|
+
# # self.token = [...]
|
|
42
|
+
# # This might be complicated...
|
|
43
|
+
|
|
44
|
+
def login(self, username, password):
|
|
45
|
+
endpoint = f"{self.server_url}/auth/jwt/login"
|
|
46
|
+
try:
|
|
47
|
+
response = requests.post(
|
|
48
|
+
endpoint,
|
|
49
|
+
data={
|
|
50
|
+
"username": username,
|
|
51
|
+
"password": password,
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
except requests.exceptions.RequestException as e:
|
|
55
|
+
raise ServerRequestError(endpoint, e)
|
|
56
|
+
|
|
57
|
+
if response.status_code == 200:
|
|
58
|
+
token = response.json().get("access_token")
|
|
59
|
+
self.token = token
|
|
60
|
+
else:
|
|
61
|
+
raise AlgorithmServerError(response.status_code, response.text)
|
|
34
62
|
|
|
35
63
|
@property
|
|
36
64
|
def server_url(self) -> str:
|
|
@@ -60,6 +88,7 @@ class Client:
|
|
|
60
88
|
headers={
|
|
61
89
|
"Content-Type": "application/json",
|
|
62
90
|
"accept": "application/json",
|
|
91
|
+
"Authorization": f"Bearer {self.token}",
|
|
63
92
|
},
|
|
64
93
|
)
|
|
65
94
|
except requests.exceptions.RequestException as e:
|
|
@@ -111,10 +140,7 @@ class Client:
|
|
|
111
140
|
return image
|
|
112
141
|
return images
|
|
113
142
|
else:
|
|
114
|
-
|
|
115
|
-
f"Error status while attempting to get algorithm sample images: {response.status_code}"
|
|
116
|
-
)
|
|
117
|
-
return -1
|
|
143
|
+
raise AlgorithmServerError(response.status_code, response.text)
|
|
118
144
|
|
|
119
145
|
def _validate_algorithm(self, algorithm=None) -> str:
|
|
120
146
|
if algorithm is None:
|
|
@@ -5,11 +5,11 @@ import numpy as np
|
|
|
5
5
|
from imantics import Mask
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def
|
|
8
|
+
def mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
|
|
9
9
|
"""
|
|
10
10
|
Args:
|
|
11
|
-
segm_mask: Segmentation mask with the background pixels set to zero and the pixels assigned to a segmented
|
|
12
|
-
|
|
11
|
+
segm_mask: Segmentation mask with the background pixels set to zero and the pixels assigned to a segmented
|
|
12
|
+
class set to an int value
|
|
13
13
|
|
|
14
14
|
Returns:
|
|
15
15
|
A list containing the contours of each object as a geojson.Feature
|
|
@@ -21,18 +21,18 @@ def instance_mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
|
|
|
21
21
|
if indices.size == 0:
|
|
22
22
|
return features
|
|
23
23
|
|
|
24
|
-
for
|
|
25
|
-
mask = segmentation_mask == int(
|
|
24
|
+
for pixel_class in indices:
|
|
25
|
+
mask = segmentation_mask == int(pixel_class)
|
|
26
26
|
polygons = Mask(mask).polygons()
|
|
27
|
-
for contour in polygons.points:
|
|
27
|
+
for detection_id, contour in enumerate(polygons.points, start=1):
|
|
28
28
|
coords = np.array(contour)
|
|
29
29
|
coords = np.vstack([coords, coords[0]]) # Close the polygon for QuPath
|
|
30
30
|
geom = Polygon(coordinates=[coords.tolist()])
|
|
31
31
|
feature = Feature(
|
|
32
32
|
geometry=geom,
|
|
33
33
|
properties={
|
|
34
|
-
"Detection ID":
|
|
35
|
-
"Class":
|
|
34
|
+
"Detection ID": detection_id,
|
|
35
|
+
"Class": int(pixel_class), # the int() casting solves a bug with json serialization
|
|
36
36
|
}
|
|
37
37
|
)
|
|
38
38
|
features.append(feature)
|
|
@@ -40,11 +40,24 @@ def instance_mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
|
|
|
40
40
|
return features
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
def
|
|
43
|
+
def features2mask(features, image_shape):
|
|
44
|
+
segmentation_mask = np.zeros(image_shape, dtype=np.uint16)
|
|
45
|
+
for feature in features:
|
|
46
|
+
feature_coordinates = np.array(feature["geometry"]["coordinates"])
|
|
47
|
+
feature_coordinates = np.squeeze(feature_coordinates)
|
|
48
|
+
feature_coordinates = feature_coordinates[:, ::-1] # Invert XY
|
|
49
|
+
feature_mask = polygon2mask(image_shape, feature_coordinates)
|
|
50
|
+
feature_properites = feature.get("properties")
|
|
51
|
+
feature_class = feature_properites.get("Class")
|
|
52
|
+
segmentation_mask[feature_mask] = feature_class
|
|
53
|
+
return segmentation_mask
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def instance_mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
|
|
44
57
|
"""
|
|
45
58
|
Args:
|
|
46
|
-
segm_mask: Segmentation mask with the background pixels set to zero and the pixels assigned to a segmented
|
|
47
|
-
|
|
59
|
+
segm_mask: Segmentation mask with the background pixels set to zero and the pixels assigned to a segmented
|
|
60
|
+
object instance set to an int value
|
|
48
61
|
|
|
49
62
|
Returns:
|
|
50
63
|
A list containing the contours of each object as a geojson.Feature
|
|
@@ -56,18 +69,18 @@ def mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
|
|
|
56
69
|
if indices.size == 0:
|
|
57
70
|
return features
|
|
58
71
|
|
|
59
|
-
for
|
|
60
|
-
mask = segmentation_mask == int(
|
|
72
|
+
for detection_id in indices:
|
|
73
|
+
mask = segmentation_mask == int(detection_id)
|
|
61
74
|
polygons = Mask(mask).polygons()
|
|
62
|
-
for
|
|
75
|
+
for contour in polygons.points:
|
|
63
76
|
coords = np.array(contour)
|
|
64
77
|
coords = np.vstack([coords, coords[0]]) # Close the polygon for QuPath
|
|
65
78
|
geom = Polygon(coordinates=[coords.tolist()])
|
|
66
79
|
feature = Feature(
|
|
67
80
|
geometry=geom,
|
|
68
81
|
properties={
|
|
69
|
-
"Detection ID": detection_id,
|
|
70
|
-
"Class":
|
|
82
|
+
"Detection ID": int(detection_id),
|
|
83
|
+
"Class": 1
|
|
71
84
|
}
|
|
72
85
|
)
|
|
73
86
|
features.append(feature)
|
|
@@ -75,7 +88,7 @@ def mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
|
|
|
75
88
|
return features
|
|
76
89
|
|
|
77
90
|
|
|
78
|
-
def
|
|
91
|
+
def features2instance_mask(features, image_shape):
|
|
79
92
|
segmentation_mask = np.zeros(image_shape, dtype=np.uint16)
|
|
80
93
|
for feature in features:
|
|
81
94
|
feature_coordinates = np.array(feature["geometry"]["coordinates"])
|
|
@@ -83,21 +96,58 @@ def features2mask(features, image_shape):
|
|
|
83
96
|
feature_coordinates = feature_coordinates[:, ::-1] # Invert XY
|
|
84
97
|
feature_mask = polygon2mask(image_shape, feature_coordinates)
|
|
85
98
|
feature_properites = feature.get("properties")
|
|
86
|
-
|
|
87
|
-
segmentation_mask[feature_mask] =
|
|
99
|
+
feature_id = feature_properites.get("Detection ID")
|
|
100
|
+
segmentation_mask[feature_mask] = feature_id
|
|
88
101
|
return segmentation_mask
|
|
89
102
|
|
|
90
103
|
|
|
91
|
-
def
|
|
104
|
+
def mask2features_3d(segmentation_mask: np.ndarray) -> List[Feature]:
|
|
105
|
+
features = []
|
|
106
|
+
for z_idx, mask_2d in enumerate(segmentation_mask):
|
|
107
|
+
features_2d = mask2features(mask_2d)
|
|
108
|
+
for feature_2d in features_2d:
|
|
109
|
+
feature_2d.properties["z_idx"] = z_idx
|
|
110
|
+
features.append(feature_2d)
|
|
111
|
+
return features
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def features2mask_3d(features, image_shape):
|
|
92
115
|
segmentation_mask = np.zeros(image_shape, dtype=np.uint16)
|
|
116
|
+
_, ry, rx = image_shape
|
|
93
117
|
for feature in features:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
feature_mask = polygon2mask(
|
|
118
|
+
feature_xy_coordinates = np.array(feature["geometry"]["coordinates"])
|
|
119
|
+
feature_xy_coordinates = np.squeeze(feature_xy_coordinates)
|
|
120
|
+
feature_xy_coordinates = feature_xy_coordinates[:, ::-1] # Invert XY
|
|
121
|
+
feature_mask = polygon2mask((ry, rx), feature_xy_coordinates)
|
|
122
|
+
feature_z_idx = feature["properties"]["z_idx"]
|
|
123
|
+
feature_properites = feature.get("properties")
|
|
124
|
+
feature_id = feature_properites.get("Class")
|
|
125
|
+
segmentation_mask[feature_z_idx][feature_mask] = feature_id
|
|
126
|
+
return segmentation_mask
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def instance_mask2features_3d(segmentation_mask: np.ndarray) -> List[Feature]:
|
|
130
|
+
features = []
|
|
131
|
+
for z_idx, mask_2d in enumerate(segmentation_mask):
|
|
132
|
+
features_2d = instance_mask2features(mask_2d)
|
|
133
|
+
for feature_2d in features_2d:
|
|
134
|
+
feature_2d.properties["z_idx"] = z_idx
|
|
135
|
+
features.append(feature_2d)
|
|
136
|
+
return features
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def features2instance_mask_3d(features, image_shape):
|
|
140
|
+
segmentation_mask = np.zeros(image_shape, dtype=np.uint16)
|
|
141
|
+
_, ry, rx = image_shape
|
|
142
|
+
for feature in features:
|
|
143
|
+
feature_xy_coordinates = np.array(feature["geometry"]["coordinates"])
|
|
144
|
+
feature_xy_coordinates = np.squeeze(feature_xy_coordinates)
|
|
145
|
+
feature_xy_coordinates = feature_xy_coordinates[:, ::-1] # Invert XY
|
|
146
|
+
feature_mask = polygon2mask((ry, rx), feature_xy_coordinates)
|
|
147
|
+
feature_z_idx = feature["properties"]["z_idx"]
|
|
98
148
|
feature_properites = feature.get("properties")
|
|
99
149
|
feature_id = feature_properites.get("Detection ID")
|
|
100
|
-
segmentation_mask[feature_mask] = feature_id
|
|
150
|
+
segmentation_mask[feature_z_idx][feature_mask] = feature_id
|
|
101
151
|
return segmentation_mask
|
|
102
152
|
|
|
103
153
|
|