imaging-server-kit 0.0.2__tar.gz → 0.0.4__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.2 → imaging_server_kit-0.0.4}/Dockerfile +2 -1
  2. imaging_server_kit-0.0.4/PKG-INFO +189 -0
  3. imaging_server_kit-0.0.4/README.md +126 -0
  4. imaging_server_kit-0.0.4/build.sh +6 -0
  5. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/main.py +4 -0
  6. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/pyproject.toml +1 -1
  7. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/__init__.py +6 -2
  8. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/_version.py +2 -2
  9. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/client.py +7 -4
  10. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/errors.py +14 -4
  11. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/geometry.py +15 -18
  12. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/registry.py +3 -3
  13. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/serialization.py +1 -0
  14. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/server.py +33 -12
  15. imaging_server_kit-0.0.4/src/imaging_server_kit/static/index.css +47 -0
  16. imaging_server_kit-0.0.4/src/imaging_server_kit/static/info.css +124 -0
  17. imaging_server_kit-0.0.4/src/imaging_server_kit/templates/index.html +33 -0
  18. imaging_server_kit-0.0.4/src/imaging_server_kit/templates/info.html +77 -0
  19. imaging_server_kit-0.0.4/src/imaging_server_kit.egg-info/PKG-INFO +189 -0
  20. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit.egg-info/SOURCES.txt +0 -2
  21. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit.egg-info/requires.txt +1 -1
  22. imaging_server_kit-0.0.4/start_registry.py +8 -0
  23. imaging_server_kit-0.0.2/Dockerfile-registry +0 -5
  24. imaging_server_kit-0.0.2/PKG-INFO +0 -154
  25. imaging_server_kit-0.0.2/README.md +0 -91
  26. imaging_server_kit-0.0.2/build.sh +0 -14
  27. imaging_server_kit-0.0.2/src/imaging_server_kit/autostartup_registry.py +0 -58
  28. imaging_server_kit-0.0.2/src/imaging_server_kit/static/index.css +0 -32
  29. imaging_server_kit-0.0.2/src/imaging_server_kit/static/info.css +0 -32
  30. imaging_server_kit-0.0.2/src/imaging_server_kit/templates/index.html +0 -25
  31. imaging_server_kit-0.0.2/src/imaging_server_kit/templates/info.html +0 -63
  32. imaging_server_kit-0.0.2/src/imaging_server_kit.egg-info/PKG-INFO +0 -154
  33. imaging_server_kit-0.0.2/start_registry.py +0 -4
  34. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/.dockerignore +0 -0
  35. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/.gitattributes +0 -0
  36. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/.github/workflows/publish.yml +0 -0
  37. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/.gitignore +0 -0
  38. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/Dockerfile-GPU +0 -0
  39. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/LICENSE +0 -0
  40. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/MANIFEST.in +0 -0
  41. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/examples/.gitkeep +0 -0
  42. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/setup.cfg +0 -0
  43. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/assets/style.css +0 -0
  44. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/encoding.py +0 -0
  45. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit/web_demo.py +0 -0
  46. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit.egg-info/dependency_links.txt +0 -0
  47. {imaging_server_kit-0.0.2 → imaging_server_kit-0.0.4}/src/imaging_server_kit.egg-info/top_level.txt +0 -0
@@ -12,4 +12,5 @@ RUN python -m pip install -e .
12
12
 
13
13
  EXPOSE 8000
14
14
 
15
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
15
+ CMD ["python3", "main.py"]
16
+ # CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.2
2
+ Name: imaging-server-kit
3
+ Version: 0.0.4
4
+ Summary: Deploy image processing algorithms in FastAPI servers.
5
+ Author-email: Mallory Wittwer <mallory.wittwer@epfl.ch>
6
+ License: Copyright (c) 2024, EPFL
7
+ All rights reserved.
8
+
9
+ Redistribution and use in source and binary forms, with or without
10
+ modification, are permitted provided that the following conditions are met:
11
+
12
+ * Redistributions of source code must retain the above copyright notice, this
13
+ list of conditions and the following disclaimer.
14
+
15
+ * Redistributions in binary form must reproduce the above copyright notice,
16
+ this list of conditions and the following disclaimer in the documentation
17
+ and/or other materials provided with the distribution.
18
+
19
+ * Neither the name of copyright holder nor the names of its
20
+ contributors may be used to endorse or promote products derived from
21
+ this software without specific prior written permission.
22
+
23
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
+
34
+ Project-URL: homepage, https://gitlab.com/epfl-center-for-imaging/imaging-server-kit
35
+ Project-URL: repository, https://gitlab.com/epfl-center-for-imaging/imaging-server-kit
36
+ Classifier: Development Status :: 2 - Pre-Alpha
37
+ Classifier: Intended Audience :: Developers
38
+ Classifier: License :: OSI Approved :: BSD License
39
+ Classifier: Operating System :: OS Independent
40
+ Classifier: Programming Language :: Python
41
+ Classifier: Programming Language :: Python :: 3
42
+ Classifier: Programming Language :: Python :: 3 :: Only
43
+ Classifier: Programming Language :: Python :: 3.9
44
+ Classifier: Programming Language :: Python :: 3.10
45
+ Classifier: Programming Language :: Python :: 3.11
46
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
47
+ Requires-Python: >=3.9
48
+ Description-Content-Type: text/markdown
49
+ License-File: LICENSE
50
+ Requires-Dist: fastapi
51
+ Requires-Dist: uvicorn[standard]
52
+ Requires-Dist: python-multipart
53
+ Requires-Dist: tifffile
54
+ Requires-Dist: numpy
55
+ Requires-Dist: scikit-image
56
+ Requires-Dist: geojson
57
+ Requires-Dist: jinja2
58
+ Requires-Dist: pyyaml
59
+ Requires-Dist: dash
60
+ Requires-Dist: dash-bootstrap-components
61
+ Requires-Dist: a2wsgi
62
+ Requires-Dist: pytest
63
+
64
+ ![EPFL Center for Imaging logo](https://imaging.epfl.ch/resources/logo-for-gitlab.svg)
65
+ # 🪐 Imaging Server Kit
66
+
67
+ Deploy image processing algorithms in FastAPI servers and easily run them from Napari, QuPath, and more.
68
+
69
+ The algorithm servers implement a common set of API endpoints for processing and to document the project. These include:
70
+ - `/process`: Runs the algorithm
71
+ - `/info`: Displays documentation about the algorithm usage
72
+ - `/sample_images`: Provides one or multiple sample images
73
+
74
+ ## Usage
75
+
76
+ ### Algorithm servers
77
+
78
+ Set up and run a server with one, or multiple algorithms from the Imaging Server Kit collection (or your own):
79
+
80
+ - [StarDist](https://github.com/Imaging-Server-Kit/serverkit-stardist): Object detection with star-convex shapes
81
+ - [CellPose](https://github.com/Imaging-Server-Kit/serverkit-cellpose): A generalist algorithm for cellular segmentation
82
+ - [Spotiflow](https://github.com/Imaging-Server-Kit/serverkit-spotiflow): Accurate and efficient spot detection
83
+ - [Rembg](https://github.com/Imaging-Server-Kit/serverkit-rembg): A tool to remove images background
84
+ - [LoG detector](https://github.com/Imaging-Server-Kit/serverkit-skimage-LoG): Laplacian of Gaussian filter
85
+ - [Orientationpy](https://github.com/Imaging-Server-Kit/serverkit-orientationpy): Measurement of greyscale orientations
86
+ <!-- - [Tau Fibrils Detector](https://github.com/Imaging-Server-Kit/serverkit-tau-fibrils-yolo) -->
87
+
88
+ All of these algorithm servers can be installed locally or built and run with docker. To use docker and serve multiple algorithms, the recommended way is to edit a `docker-compose.yml` file and pull server images from `registry.rcp.epfl.ch` (EPFL users only):
89
+
90
+ ```{docker-compose.yml}
91
+ services:
92
+ servers_registry:
93
+ image: mallorywittwerepfl/imaging-server-kit:registry
94
+ ports:
95
+ - "8000:8000"
96
+ rembg:
97
+ image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-rembg:latest
98
+ depends_on:
99
+ - servers_registry
100
+ skimage-log:
101
+ image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-skimage-log:latest
102
+ depends_on:
103
+ - servers_registry
104
+ ```
105
+
106
+ Start the server with
107
+
108
+ ```
109
+ docker compose up
110
+ ```
111
+
112
+ The server should be accessible at http://localhost:8000.
113
+
114
+ **Build the docker images yourself**
115
+
116
+ To build the algorithm server images yourself (e.g. if you are not from EPFL), see [Reference Deployment](https://github.com/Imaging-Server-Kit/serverkit-deploy-docker).
117
+
118
+ **Create a server for your project**
119
+
120
+ To learn how to create a server for your project, see [Algorithm Server Template](https://github.com/Imaging-Server-Kit/cookiecutter-serverkit).
121
+
122
+ ### Python client
123
+
124
+ Once the server is running, connect to it and run algorithms from Python:
125
+
126
+ ```python
127
+ from imaging_server_kit import Client
128
+
129
+ client = Client("http://localhost:8000")
130
+
131
+ print(client.algorithms)
132
+ # [`rembg`, `stardist`, `cellpose`]
133
+
134
+ algo_output = client.run_algorithm(
135
+ algorithm="rembg",
136
+ image=(...),
137
+ rembg_model_name="silueta",
138
+ )
139
+ ```
140
+
141
+ ### Napari plugin
142
+
143
+ Once the server is running, use the [Napari Server Kit](https://github.com/Imaging-Server-Kit/napari-serverkit) plugin to connect to it and run algorithms in Napari.
144
+
145
+ ### QuPath extension
146
+
147
+ Once the server is running, use the [QuPath Extension Server Kit](https://github.com/Imaging-Server-Kit/qupath-extension-serverkit) to connect to it and run algorithms from within QuPath.
148
+
149
+ ## Install locally
150
+
151
+ Install the `imaging-server-kit` package with `pip`:
152
+
153
+ ```
154
+ pip install imaging-server-kit
155
+ ```
156
+
157
+ or clone the project and install the development version:
158
+
159
+ ```
160
+ git clone https://github.com/Imaging-Server-Kit/imaging-server-kit.git
161
+ cd imaging-server-kit
162
+ pip install -e .
163
+ ```
164
+
165
+ ## Build with `docker`
166
+
167
+ Build the `imaging-server-kit` docker image for a specific Python version:
168
+
169
+ ```
170
+ docker build --build-arg PYTHON_VERSION=3.9 -t imaging-server-kit:3.9 .
171
+ ```
172
+
173
+ Run [build.sh](./build.sh) to build docker images for Python 3.9, 3.10, 3.11, GPU and the server kit registry:
174
+
175
+ ```
176
+ bash ./build.sh
177
+ ```
178
+
179
+ ## Contributing
180
+
181
+ Contributions are very welcome.
182
+
183
+ ## License
184
+
185
+ This software is distributed under the terms of the [BSD-3](http://opensource.org/licenses/BSD-3-Clause) license.
186
+
187
+ ## Issues
188
+
189
+ If you encounter any problems, please file an issue along with a detailed description.
@@ -0,0 +1,126 @@
1
+ ![EPFL Center for Imaging logo](https://imaging.epfl.ch/resources/logo-for-gitlab.svg)
2
+ # 🪐 Imaging Server Kit
3
+
4
+ Deploy image processing algorithms in FastAPI servers and easily run them from Napari, QuPath, and more.
5
+
6
+ The algorithm servers implement a common set of API endpoints for processing and to document the project. These include:
7
+ - `/process`: Runs the algorithm
8
+ - `/info`: Displays documentation about the algorithm usage
9
+ - `/sample_images`: Provides one or multiple sample images
10
+
11
+ ## Usage
12
+
13
+ ### Algorithm servers
14
+
15
+ Set up and run a server with one, or multiple algorithms from the Imaging Server Kit collection (or your own):
16
+
17
+ - [StarDist](https://github.com/Imaging-Server-Kit/serverkit-stardist): Object detection with star-convex shapes
18
+ - [CellPose](https://github.com/Imaging-Server-Kit/serverkit-cellpose): A generalist algorithm for cellular segmentation
19
+ - [Spotiflow](https://github.com/Imaging-Server-Kit/serverkit-spotiflow): Accurate and efficient spot detection
20
+ - [Rembg](https://github.com/Imaging-Server-Kit/serverkit-rembg): A tool to remove images background
21
+ - [LoG detector](https://github.com/Imaging-Server-Kit/serverkit-skimage-LoG): Laplacian of Gaussian filter
22
+ - [Orientationpy](https://github.com/Imaging-Server-Kit/serverkit-orientationpy): Measurement of greyscale orientations
23
+ <!-- - [Tau Fibrils Detector](https://github.com/Imaging-Server-Kit/serverkit-tau-fibrils-yolo) -->
24
+
25
+ All of these algorithm servers can be installed locally or built and run with docker. To use docker and serve multiple algorithms, the recommended way is to edit a `docker-compose.yml` file and pull server images from `registry.rcp.epfl.ch` (EPFL users only):
26
+
27
+ ```{docker-compose.yml}
28
+ services:
29
+ servers_registry:
30
+ image: mallorywittwerepfl/imaging-server-kit:registry
31
+ ports:
32
+ - "8000:8000"
33
+ rembg:
34
+ image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-rembg:latest
35
+ depends_on:
36
+ - servers_registry
37
+ skimage-log:
38
+ image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-skimage-log:latest
39
+ depends_on:
40
+ - servers_registry
41
+ ```
42
+
43
+ Start the server with
44
+
45
+ ```
46
+ docker compose up
47
+ ```
48
+
49
+ The server should be accessible at http://localhost:8000.
50
+
51
+ **Build the docker images yourself**
52
+
53
+ To build the algorithm server images yourself (e.g. if you are not from EPFL), see [Reference Deployment](https://github.com/Imaging-Server-Kit/serverkit-deploy-docker).
54
+
55
+ **Create a server for your project**
56
+
57
+ To learn how to create a server for your project, see [Algorithm Server Template](https://github.com/Imaging-Server-Kit/cookiecutter-serverkit).
58
+
59
+ ### Python client
60
+
61
+ Once the server is running, connect to it and run algorithms from Python:
62
+
63
+ ```python
64
+ from imaging_server_kit import Client
65
+
66
+ client = Client("http://localhost:8000")
67
+
68
+ print(client.algorithms)
69
+ # [`rembg`, `stardist`, `cellpose`]
70
+
71
+ algo_output = client.run_algorithm(
72
+ algorithm="rembg",
73
+ image=(...),
74
+ rembg_model_name="silueta",
75
+ )
76
+ ```
77
+
78
+ ### Napari plugin
79
+
80
+ Once the server is running, use the [Napari Server Kit](https://github.com/Imaging-Server-Kit/napari-serverkit) plugin to connect to it and run algorithms in Napari.
81
+
82
+ ### QuPath extension
83
+
84
+ Once the server is running, use the [QuPath Extension Server Kit](https://github.com/Imaging-Server-Kit/qupath-extension-serverkit) to connect to it and run algorithms from within QuPath.
85
+
86
+ ## Install locally
87
+
88
+ Install the `imaging-server-kit` package with `pip`:
89
+
90
+ ```
91
+ pip install imaging-server-kit
92
+ ```
93
+
94
+ or clone the project and install the development version:
95
+
96
+ ```
97
+ git clone https://github.com/Imaging-Server-Kit/imaging-server-kit.git
98
+ cd imaging-server-kit
99
+ pip install -e .
100
+ ```
101
+
102
+ ## Build with `docker`
103
+
104
+ Build the `imaging-server-kit` docker image for a specific Python version:
105
+
106
+ ```
107
+ docker build --build-arg PYTHON_VERSION=3.9 -t imaging-server-kit:3.9 .
108
+ ```
109
+
110
+ Run [build.sh](./build.sh) to build docker images for Python 3.9, 3.10, 3.11, GPU and the server kit registry:
111
+
112
+ ```
113
+ bash ./build.sh
114
+ ```
115
+
116
+ ## Contributing
117
+
118
+ Contributions are very welcome.
119
+
120
+ ## License
121
+
122
+ This software is distributed under the terms of the [BSD-3](http://opensource.org/licenses/BSD-3-Clause) license.
123
+
124
+ ## Issues
125
+
126
+ If you encounter any problems, please file an issue along with a detailed description.
@@ -0,0 +1,6 @@
1
+ #!/bin/bash
2
+
3
+ docker build --build-arg PYTHON_VERSION=3.9 -t imaging-server-kit .
4
+
5
+ # echo "Building image for Python 3.9, GPU-compatible..."
6
+ # docker build -t imaging-server-kit:gpu --file Dockerfile-GPU .
@@ -1,4 +1,8 @@
1
+ import uvicorn
1
2
  import imaging_server_kit as serverkit
2
3
 
3
4
  server = serverkit.Server(algorithm_name="foo", parameters_model=serverkit.Parameters)
4
5
  app = server.app
6
+
7
+ if __name__=='__main__':
8
+ uvicorn.run("main:app", host="0.0.0.0", port=8000)
@@ -29,11 +29,11 @@ dependencies = [
29
29
  "numpy",
30
30
  "scikit-image",
31
31
  "geojson",
32
- "docker", # For the autostartup regsitry
33
32
  "jinja2", # For the /info route
34
33
  "pyyaml", # For the /info route
35
34
  "dash", # For the dash app
36
35
  "dash-bootstrap-components", # For the dash app
36
+ "a2wsgi", # For the dash app
37
37
  "pytest", # For testing
38
38
  ]
39
39
 
@@ -1,8 +1,12 @@
1
- from imaging_server_kit._version import __version__
1
+ from ._version import version as __version__
2
+ try:
3
+ from ._version import version as __version__
4
+ except ImportError:
5
+ __version__ = "unknown"
6
+
2
7
  from imaging_server_kit.client import Client
3
8
  from imaging_server_kit.server import Server, Parameters
4
9
  from imaging_server_kit.registry import Registry
5
- from imaging_server_kit.autostartup_registry import AutoStartupRegistry
6
10
  from imaging_server_kit.encoding import encode_contents, decode_contents
7
11
  from imaging_server_kit.serialization import (
8
12
  serialize_result_tuple,
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.0.2'
16
- __version_tuple__ = version_tuple = (0, 0, 2)
15
+ __version__ = version = '0.0.4'
16
+ __version_tuple__ = version_tuple = (0, 0, 4)
@@ -7,6 +7,7 @@ from imaging_server_kit.errors import (
7
7
  AlgorithmServerError,
8
8
  InvalidAlgorithmParametersError,
9
9
  ServerRequestError,
10
+ AlgorithmTimeoutError,
10
11
  )
11
12
 
12
13
 
@@ -29,7 +30,7 @@ class Client:
29
30
  services = response.json().get("services")
30
31
  self.algorithms = services
31
32
  else:
32
- raise AlgorithmServerError(response.status_code, response.json())
33
+ raise AlgorithmServerError(response.status_code, response.text)
33
34
 
34
35
  @property
35
36
  def server_url(self) -> str:
@@ -52,7 +53,7 @@ class Client:
52
53
  algo_params_encoded = self._encode_numpy_parameters(algo_params)
53
54
 
54
55
  try:
55
- endpoint = f"{self.server_url}/{algorithm}/"
56
+ endpoint = f"{self.server_url}/{algorithm}/process"
56
57
  response = requests.post(
57
58
  endpoint,
58
59
  json=algo_params_encoded,
@@ -68,8 +69,10 @@ class Client:
68
69
  return serverkit.deserialize_result_tuple(response.json())
69
70
  elif response.status_code == 422:
70
71
  raise InvalidAlgorithmParametersError(response.status_code, response.json())
72
+ elif response.status_code == 504:
73
+ raise AlgorithmTimeoutError(response.status_code, response.text)
71
74
  else:
72
- raise AlgorithmServerError(response.status_code, response.json())
75
+ raise AlgorithmServerError(response.status_code, response.text)
73
76
 
74
77
  def get_algorithm_parameters(self, algorithm=None) -> Dict:
75
78
  algorithm = self._validate_algorithm(algorithm)
@@ -84,7 +87,7 @@ class Client:
84
87
  if response.status_code == 200:
85
88
  return response.json()
86
89
  else:
87
- raise AlgorithmServerError(response.status_code, response.json())
90
+ raise AlgorithmServerError(response.status_code, response.text)
88
91
 
89
92
  def get_sample_images(
90
93
  self, algorithm=None, first_only: bool = False
@@ -13,17 +13,27 @@ class AlgorithmServerError(Exception):
13
13
  def __init__(self, status_code, response_body, message="Algorithm server returned an unexpected status_code"):
14
14
  self.status_code = status_code
15
15
  self.response_body = response_body
16
- self.message = f"{message}: {status_code}\nResponse body: {response_body}"
16
+ self.message = f"{message}: {status_code}\nResponse: {response_body}"
17
+ super().__init__(self.message)
18
+
19
+
20
+ class AlgorithmTimeoutError(Exception):
21
+ """Exception raised when a request to an algorithm server exceeds the time limit."""
22
+
23
+ def __init__(self, status_code, response_text, message="Algorithm server returned an unexpected status_code"):
24
+ self.status_code = status_code
25
+ self.response_text = response_text
26
+ self.message = f"{message}: {status_code}\nResponse: {response_text}"
17
27
  super().__init__(self.message)
18
28
 
19
29
 
20
30
  class InvalidAlgorithmParametersError(Exception):
21
31
  """Exception raised when a request to an algorithm server is made with invalid algorithm parameters."""
22
32
 
23
- def __init__(self, status_code, response_body, message="Algorithm parameters were invalidated by the server"):
33
+ def __init__(self, status_code, response_text, message="Algorithm parameters were invalidated by the server"):
24
34
  self.status_code = status_code
25
- self.response_body = response_body
26
- pydantic_details = response_body.get("detail")[0]
35
+ self.response_text = response_text
36
+ pydantic_details = response_text.get("detail")[0]
27
37
  pydantic_validation_msg = pydantic_details.get("msg")
28
38
  pydantic_failing_param = pydantic_details.get("loc")[1]
29
39
  pydantic_failing_param_value = pydantic_details.get("input")
@@ -17,37 +17,34 @@ def mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
17
17
  features = []
18
18
  indices = np.unique(segmentation_mask)
19
19
  indices = np.delete(indices, indices == 0) # remove background
20
- if indices.size > 0:
21
- for i in indices.tolist():
22
- mask = np.array(segmentation_mask == i)
23
- mask = np.pad(mask > 0, 1)
24
- contours_find = find_contours(mask, 0.5)
25
- if len(contours_find) == 1:
26
- index = 0
27
- else:
28
- # TODO: Change this to handle panoptic segmentation cases
29
- # if more than one contour is found, the largest one is returned
30
- n_pixels = []
31
- for _, item in enumerate(contours_find):
32
- n_pixels.append(len(item))
33
- index = np.argmax(n_pixels)
34
- contour = contours_find[index]
20
+
21
+ if indices.size == 0:
22
+ return features
23
+
24
+ # TODO: This is really inefficient for big images! Will need to improve it.
25
+ for i in indices.tolist():
26
+ mask = np.array(segmentation_mask == i)
27
+ mask = np.pad(mask > 0, 1)
28
+ contours_find = find_contours(mask, 0.5)
29
+ for detection_id, contour in enumerate(contours_find):
35
30
  contour -= 1 # reset padding
36
31
  contour_as_numpy = contour[:, np.argsort([1, 0])] # Fix XY
37
32
  geom = Polygon([contour_as_numpy.tolist()])
38
- features.append(Feature(geometry=geom, properties={"Detection ID": i}))
33
+ features.append(Feature(geometry=geom, properties={"Detection ID": detection_id, "Class": i}))
34
+
39
35
  return features
40
36
 
41
37
 
42
38
  def features2mask(features, image_shape):
43
39
  segmentation_mask = np.zeros(image_shape, dtype=np.uint16)
44
40
  for idx, feature in enumerate(features):
45
- label_idx = idx+1 # Labels start at 1
46
41
  feature_coordinates = np.array(feature["geometry"]["coordinates"])
47
42
  feature_coordinates = np.squeeze(feature_coordinates)
48
43
  feature_coordinates = feature_coordinates[:, ::-1] # Invert XY
49
44
  feature_mask = polygon2mask(image_shape, feature_coordinates)
50
- segmentation_mask[feature_mask] = label_idx
45
+ feature_properites = feature.get("properties")
46
+ feature_class = feature_properites.get("Class")
47
+ segmentation_mask[feature_mask] = feature_class
51
48
  return segmentation_mask
52
49
 
53
50
 
@@ -74,17 +74,17 @@ class Registry:
74
74
  # response = requests.get(f"{self.services.get(algorithm)}/demo")
75
75
  # return HTMLResponse(content=response.text, status_code=response.status_code)
76
76
 
77
- @self.app.get("/{algorithm}")
77
+ @self.app.get("/{algorithm}/")
78
78
  async def get_algorithm(request: Request, algorithm: str):
79
79
  algo_url = self.services.get(algorithm)
80
80
  response = requests.get(f"{algo_url}/")
81
81
  return response.json()
82
82
 
83
- @self.app.post("/{algorithm}", status_code=status.HTTP_201_CREATED)
83
+ @self.app.post("/{algorithm}/process", status_code=status.HTTP_201_CREATED)
84
84
  async def run_algorithm(algorithm, request: Request):
85
85
  algo_url = self.services.get(algorithm)
86
86
  data = await request.json()
87
- response = requests.post(f"{algo_url}/{algorithm}/", json=data)
87
+ response = requests.post(f"{algo_url}/{algorithm}/process", json=data)
88
88
  return response.json()
89
89
 
90
90
  @self.app.get("/{algorithm}/parameters")
@@ -69,6 +69,7 @@ def serialize_result_tuple(result_data_tuple: List[Tuple]) -> List[Dict]:
69
69
  if data_type == "image":
70
70
  features = serverkit.encode_contents(data.astype(np.float32))
71
71
  elif data_type == "labels":
72
+ data = data.astype(np.uint16)
72
73
  features = serverkit.mask2features(data)
73
74
  data_params['image_shape'] = data.shape
74
75
  elif data_type == "labels3d":
@@ -5,13 +5,15 @@ from typing import List, Tuple, Type
5
5
  import imaging_server_kit as serverkit
6
6
  import requests
7
7
  import yaml
8
- from fastapi import FastAPI, Request, status
8
+ from fastapi import FastAPI, Request, status, HTTPException
9
9
  from fastapi.responses import HTMLResponse
10
10
  from fastapi.staticfiles import StaticFiles
11
11
  from fastapi.templating import Jinja2Templates
12
12
  from imaging_server_kit.web_demo import generate_dash_app
13
13
  from pydantic import BaseModel, ConfigDict
14
- from starlette.middleware.wsgi import WSGIMiddleware
14
+ from a2wsgi import WSGIMiddleware
15
+ from contextlib import asynccontextmanager
16
+ import asyncio
15
17
 
16
18
  templates_dir = importlib.resources.files("imaging_server_kit").joinpath("templates")
17
19
  static_dir = importlib.resources.files("imaging_server_kit").joinpath("static")
@@ -47,7 +49,7 @@ class Server:
47
49
  self.algorithm_name = algorithm_name
48
50
  self.parameters_model = parameters_model
49
51
 
50
- self.app = FastAPI(title=algorithm_name)
52
+ self.app = FastAPI(title=algorithm_name, lifespan=self.lifespan)
51
53
  self.app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
52
54
 
53
55
  # Set up the web demo app
@@ -63,14 +65,17 @@ class Server:
63
65
 
64
66
  self.app.mount(f"/{algorithm_name}/demo", WSGIMiddleware(dash_app.server))
65
67
 
66
- self.app.on_event("startup")(self.register_with_registry)
67
- self.app.on_event("shutdown")(self.deregister_from_registry)
68
-
69
68
  self.register_routes()
70
69
 
71
70
  self.services = [self.algorithm_name]
72
71
 
73
- def register_with_registry(self):
72
+ @asynccontextmanager
73
+ async def lifespan(self, app: FastAPI):
74
+ await self.register_with_registry()
75
+ yield
76
+ await self.deregister_from_registry()
77
+
78
+ async def register_with_registry(self):
74
79
  try:
75
80
  response = requests.get(f"{REGISTRY_URL}/")
76
81
  except Exception:
@@ -89,7 +94,7 @@ class Server:
89
94
  else:
90
95
  print(f"Failed to register {self.algorithm_name}: {response.json()}")
91
96
 
92
- def deregister_from_registry(self):
97
+ async def deregister_from_registry(self):
93
98
  deregister_url = f"{REGISTRY_URL}/deregister"
94
99
  response = requests.post(deregister_url, json={"name": self.algorithm_name})
95
100
  if response.status_code == 201:
@@ -124,12 +129,20 @@ class Server:
124
129
  },
125
130
  )
126
131
 
127
- @self.app.post(f"/{self.algorithm_name}/", status_code=status.HTTP_201_CREATED)
132
+ @self.app.post(f"/{self.algorithm_name}/process", status_code=status.HTTP_201_CREATED)
128
133
  async def run_algo(algo_params: self.parameters_model):
129
- result_data_tuple = self.run_algorithm(**algo_params.dict())
130
- serialized_results = serverkit.serialize_result_tuple(result_data_tuple)
134
+ try:
135
+ result_data_tuple = await asyncio.wait_for(self._run_algorithm(**algo_params.dict()), timeout=60)
136
+ except asyncio.TimeoutError: # This works but doesn't actually kill the process... we need something else to terminate it
137
+ raise HTTPException(status_code=504, detail="Request timeout during processing.")
138
+ try:
139
+ serialized_results = await asyncio.wait_for(self._serialize_result_tuple(result_data_tuple), timeout=10)
140
+ except asyncio.TimeoutError:
141
+ raise HTTPException(status_code=504, detail="Request timeout during serialization.")
142
+
131
143
  return serialized_results
132
144
 
145
+
133
146
  @self.app.get(f"/{self.algorithm_name}/parameters", response_model=dict)
134
147
  def get_algo_params():
135
148
  return self.parameters_model.model_json_schema()
@@ -142,8 +155,16 @@ class Server:
142
155
  ]
143
156
  return {"sample_images": encoded_images}
144
157
 
158
+ async def _serialize_result_tuple(self, result_data_tuple):
159
+ serialized_results = await asyncio.to_thread(serverkit.serialize_result_tuple, result_data_tuple)
160
+ return serialized_results
161
+
162
+ async def _run_algorithm(self, **algo_params):
163
+ result_data_tuple = await asyncio.to_thread(self.run_algorithm, **algo_params)
164
+ return result_data_tuple
165
+
145
166
  def load_sample_images(self) -> List["np.ndarray"]:
146
167
  raise NotImplementedError("Subclasses should implement this method")
147
-
168
+
148
169
  def run_algorithm(self, **algo_params) -> List[Tuple]:
149
170
  raise NotImplementedError("Subclasses should implement this method")