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.
Files changed (47) hide show
  1. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/Dockerfile +1 -2
  2. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/Dockerfile-GPU +1 -1
  3. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/PKG-INFO +10 -7
  4. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/README.md +5 -5
  5. imaging_server_kit-0.0.6/main.py +10 -0
  6. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/pyproject.toml +2 -0
  7. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/__init__.py +8 -2
  8. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/_version.py +2 -2
  9. 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
  10. imaging_server_kit-0.0.6/src/imaging_server_kit/authenticated_server.py +249 -0
  11. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/client.py +30 -4
  12. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/geometry.py +75 -25
  13. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/serialization.py +4 -4
  14. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/server.py +31 -15
  15. imaging_server_kit-0.0.6/src/imaging_server_kit/static/js/login_script.js +24 -0
  16. imaging_server_kit-0.0.6/src/imaging_server_kit/static/js/register_script.js +15 -0
  17. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/templates/index.html +1 -1
  18. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/templates/info.html +1 -1
  19. imaging_server_kit-0.0.6/src/imaging_server_kit/templates/login.html +27 -0
  20. imaging_server_kit-0.0.6/src/imaging_server_kit/templates/register.html +27 -0
  21. imaging_server_kit-0.0.6/src/imaging_server_kit/users_utils/__init__.py +3 -0
  22. imaging_server_kit-0.0.6/src/imaging_server_kit/users_utils/db.py +34 -0
  23. imaging_server_kit-0.0.6/src/imaging_server_kit/users_utils/schemas.py +15 -0
  24. imaging_server_kit-0.0.6/src/imaging_server_kit/users_utils/users.py +55 -0
  25. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/web_demo.py +6 -6
  26. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/PKG-INFO +10 -7
  27. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/SOURCES.txt +14 -5
  28. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/requires.txt +2 -0
  29. imaging_server_kit-0.0.6/start_algorithm_hub.py +8 -0
  30. imaging_server_kit-0.0.5/main.py +0 -8
  31. imaging_server_kit-0.0.5/start_registry.py +0 -8
  32. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/.dockerignore +0 -0
  33. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/.gitattributes +0 -0
  34. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/.github/workflows/publish.yml +0 -0
  35. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/.gitignore +0 -0
  36. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/LICENSE +0 -0
  37. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/MANIFEST.in +0 -0
  38. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/build.sh +0 -0
  39. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/examples/.gitkeep +0 -0
  40. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/setup.cfg +0 -0
  41. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/assets/style.css +0 -0
  42. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/encoding.py +0 -0
  43. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit/errors.py +0 -0
  44. {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
  45. {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
  46. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/dependency_links.txt +0 -0
  47. {imaging_server_kit-0.0.5 → imaging_server_kit-0.0.6}/src/imaging_server_kit.egg-info/top_level.txt +0 -0
@@ -12,5 +12,4 @@ RUN python -m pip install -e .
12
12
 
13
13
  EXPOSE 8000
14
14
 
15
- CMD ["python3", "main.py"]
16
- # CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
15
+ CMD ["python3", "main.py"]
@@ -11,4 +11,4 @@ RUN python -m pip install -e .
11
11
 
12
12
  EXPOSE 8000
13
13
 
14
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
14
+ CMD ["python3", "main.py"]
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: imaging-server-kit
3
- Version: 0.0.5
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
  ![EPFL Center for Imaging logo](https://imaging.epfl.ch/resources/logo-for-gitlab.svg)
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
- servers_registry:
97
+ algorithm_hub:
95
98
  image: mallorywittwerepfl/imaging-server-kit:latest
96
99
  ports:
97
100
  - "8000:8000"
98
- command: python3 start_registry.py
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
- - servers_registry
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
- - servers_registry
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 server kit registry:
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
- servers_registry:
29
+ algorithm_hub:
30
30
  image: mallorywittwerepfl/imaging-server-kit:latest
31
31
  ports:
32
32
  - "8000:8000"
33
- command: python3 start_registry.py
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
- - servers_registry
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
- - servers_registry
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 server kit registry:
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)
@@ -25,6 +25,8 @@ dependencies = [
25
25
  "fastapi",
26
26
  "uvicorn[standard]",
27
27
  "python-multipart",
28
+ "fastapi-users[sqlalchemy]",
29
+ "aiosqlite",
28
30
  "tifffile",
29
31
  "numpy",
30
32
  "scikit-image",
@@ -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 Server, Parameters
9
- from imaging_server_kit.registry import Registry
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,
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.0.5'
21
- __version_tuple__ = version_tuple = (0, 0, 5)
20
+ __version__ = version = '0.0.6'
21
+ __version_tuple__ = version_tuple = (0, 0, 6)
@@ -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 Registry:
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
- print(
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 instance_mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
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
- object instance set to an int value
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 detection_id in indices:
25
- mask = segmentation_mask == int(detection_id)
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": int(detection_id),
35
- "Class": 1
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 mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
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
- class set to an int value
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 pixel_class in indices:
60
- mask = segmentation_mask == int(pixel_class)
72
+ for detection_id in indices:
73
+ mask = segmentation_mask == int(detection_id)
61
74
  polygons = Mask(mask).polygons()
62
- for detection_id, contour in enumerate(polygons.points, start=1):
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": pixel_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 features2mask(features, image_shape):
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
- feature_class = feature_properites.get("Class")
87
- segmentation_mask[feature_mask] = feature_class
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 features2instance_mask(features, image_shape):
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
- feature_coordinates = np.array(feature["geometry"]["coordinates"])
95
- feature_coordinates = np.squeeze(feature_coordinates)
96
- feature_coordinates = feature_coordinates[:, ::-1] # Invert XY
97
- feature_mask = polygon2mask(image_shape, feature_coordinates)
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