imaging-server-kit 0.0.4__tar.gz → 0.0.5__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 (36) hide show
  1. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/PKG-INFO +7 -4
  2. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/README.md +2 -1
  3. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/pyproject.toml +4 -2
  4. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/__init__.py +2 -0
  5. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/_version.py +9 -4
  6. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/geometry.py +68 -15
  7. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/serialization.py +16 -0
  8. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/server.py +37 -14
  9. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit.egg-info/PKG-INFO +7 -4
  10. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit.egg-info/requires.txt +2 -0
  11. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/.dockerignore +0 -0
  12. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/.gitattributes +0 -0
  13. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/.github/workflows/publish.yml +0 -0
  14. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/.gitignore +0 -0
  15. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/Dockerfile +0 -0
  16. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/Dockerfile-GPU +0 -0
  17. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/LICENSE +0 -0
  18. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/MANIFEST.in +0 -0
  19. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/build.sh +0 -0
  20. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/examples/.gitkeep +0 -0
  21. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/main.py +0 -0
  22. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/setup.cfg +0 -0
  23. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/assets/style.css +0 -0
  24. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/client.py +0 -0
  25. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/encoding.py +0 -0
  26. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/errors.py +0 -0
  27. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/registry.py +0 -0
  28. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/static/index.css +0 -0
  29. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/static/info.css +0 -0
  30. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/templates/index.html +0 -0
  31. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/templates/info.html +0 -0
  32. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit/web_demo.py +0 -0
  33. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit.egg-info/SOURCES.txt +0 -0
  34. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit.egg-info/dependency_links.txt +0 -0
  35. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/src/imaging_server_kit.egg-info/top_level.txt +0 -0
  36. {imaging_server_kit-0.0.4 → imaging_server_kit-0.0.5}/start_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: imaging-server-kit
3
- Version: 0.0.4
3
+ Version: 0.0.5
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
@@ -31,8 +31,8 @@ License: Copyright (c) 2024, EPFL
31
31
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32
32
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
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
34
+ Project-URL: homepage, https://github.com/Imaging-Server-Kit/imaging-server-kit
35
+ Project-URL: repository, https://github.com/Imaging-Server-Kit/imaging-server-kit
36
36
  Classifier: Development Status :: 2 - Pre-Alpha
37
37
  Classifier: Intended Audience :: Developers
38
38
  Classifier: License :: OSI Approved :: BSD License
@@ -53,6 +53,8 @@ Requires-Dist: python-multipart
53
53
  Requires-Dist: tifffile
54
54
  Requires-Dist: numpy
55
55
  Requires-Dist: scikit-image
56
+ Requires-Dist: opencv-contrib-python-headless
57
+ Requires-Dist: imantics
56
58
  Requires-Dist: geojson
57
59
  Requires-Dist: jinja2
58
60
  Requires-Dist: pyyaml
@@ -90,9 +92,10 @@ All of these algorithm servers can be installed locally or built and run with do
90
92
  ```{docker-compose.yml}
91
93
  services:
92
94
  servers_registry:
93
- image: mallorywittwerepfl/imaging-server-kit:registry
95
+ image: mallorywittwerepfl/imaging-server-kit:latest
94
96
  ports:
95
97
  - "8000:8000"
98
+ command: python3 start_registry.py
96
99
  rembg:
97
100
  image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-rembg:latest
98
101
  depends_on:
@@ -27,9 +27,10 @@ All of these algorithm servers can be installed locally or built and run with do
27
27
  ```{docker-compose.yml}
28
28
  services:
29
29
  servers_registry:
30
- image: mallorywittwerepfl/imaging-server-kit:registry
30
+ image: mallorywittwerepfl/imaging-server-kit:latest
31
31
  ports:
32
32
  - "8000:8000"
33
+ command: python3 start_registry.py
33
34
  rembg:
34
35
  image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-rembg:latest
35
36
  depends_on:
@@ -28,6 +28,8 @@ dependencies = [
28
28
  "tifffile",
29
29
  "numpy",
30
30
  "scikit-image",
31
+ "opencv-contrib-python-headless",
32
+ "imantics",
31
33
  "geojson",
32
34
  "jinja2", # For the /info route
33
35
  "pyyaml", # For the /info route
@@ -38,8 +40,8 @@ dependencies = [
38
40
  ]
39
41
 
40
42
  [project.urls]
41
- homepage = "https://gitlab.com/epfl-center-for-imaging/imaging-server-kit"
42
- repository = "https://gitlab.com/epfl-center-for-imaging/imaging-server-kit"
43
+ homepage = "https://github.com/Imaging-Server-Kit/imaging-server-kit"
44
+ repository = "https://github.com/Imaging-Server-Kit/imaging-server-kit"
43
45
 
44
46
  [build-system]
45
47
  requires = ["setuptools>=42.0.0", "wheel", "setuptools_scm"]
@@ -14,7 +14,9 @@ from imaging_server_kit.serialization import (
14
14
  )
15
15
  from imaging_server_kit.geometry import (
16
16
  mask2features,
17
+ instance_mask2features,
17
18
  features2mask,
19
+ features2instance_mask,
18
20
  boxes2features,
19
21
  features2boxes,
20
22
  points2features,
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.0.4'
16
- __version_tuple__ = version_tuple = (0, 0, 4)
20
+ __version__ = version = '0.0.5'
21
+ __version_tuple__ = version_tuple = (0, 0, 5)
@@ -1,43 +1,83 @@
1
1
  from typing import List
2
- from skimage.measure import find_contours
3
2
  from skimage.draw import polygon2mask
4
3
  from geojson import Feature, Polygon, Point, LineString
5
4
  import numpy as np
5
+ from imantics import Mask
6
6
 
7
7
 
8
- def mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
8
+ def instance_mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
9
9
  """
10
10
  Args:
11
11
  segm_mask: Segmentation mask with the background pixels set to zero and the pixels assigned to a segmented
12
- object set to an int value
12
+ object instance set to an int value
13
13
 
14
14
  Returns:
15
15
  A list containing the contours of each object as a geojson.Feature
16
16
  """
17
17
  features = []
18
18
  indices = np.unique(segmentation_mask)
19
- indices = np.delete(indices, indices == 0) # remove background
19
+ indices = indices[indices != 0] # remove background
20
20
 
21
21
  if indices.size == 0:
22
22
  return features
23
+
24
+ for detection_id in indices:
25
+ mask = segmentation_mask == int(detection_id)
26
+ polygons = Mask(mask).polygons()
27
+ for contour in polygons.points:
28
+ coords = np.array(contour)
29
+ coords = np.vstack([coords, coords[0]]) # Close the polygon for QuPath
30
+ geom = Polygon(coordinates=[coords.tolist()])
31
+ feature = Feature(
32
+ geometry=geom,
33
+ properties={
34
+ "Detection ID": int(detection_id),
35
+ "Class": 1
36
+ }
37
+ )
38
+ features.append(feature)
39
+
40
+ return features
41
+
23
42
 
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):
30
- contour -= 1 # reset padding
31
- contour_as_numpy = contour[:, np.argsort([1, 0])] # Fix XY
32
- geom = Polygon([contour_as_numpy.tolist()])
33
- features.append(Feature(geometry=geom, properties={"Detection ID": detection_id, "Class": i}))
43
+ def mask2features(segmentation_mask: np.ndarray) -> List[Feature]:
44
+ """
45
+ 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
48
+
49
+ Returns:
50
+ A list containing the contours of each object as a geojson.Feature
51
+ """
52
+ features = []
53
+ indices = np.unique(segmentation_mask)
54
+ indices = indices[indices != 0] # remove background
55
+
56
+ if indices.size == 0:
57
+ return features
58
+
59
+ for pixel_class in indices:
60
+ mask = segmentation_mask == int(pixel_class)
61
+ polygons = Mask(mask).polygons()
62
+ for detection_id, contour in enumerate(polygons.points, start=1):
63
+ coords = np.array(contour)
64
+ coords = np.vstack([coords, coords[0]]) # Close the polygon for QuPath
65
+ geom = Polygon(coordinates=[coords.tolist()])
66
+ feature = Feature(
67
+ geometry=geom,
68
+ properties={
69
+ "Detection ID": detection_id,
70
+ "Class": pixel_class
71
+ }
72
+ )
73
+ features.append(feature)
34
74
 
35
75
  return features
36
76
 
37
77
 
38
78
  def features2mask(features, image_shape):
39
79
  segmentation_mask = np.zeros(image_shape, dtype=np.uint16)
40
- for idx, feature in enumerate(features):
80
+ for feature in features:
41
81
  feature_coordinates = np.array(feature["geometry"]["coordinates"])
42
82
  feature_coordinates = np.squeeze(feature_coordinates)
43
83
  feature_coordinates = feature_coordinates[:, ::-1] # Invert XY
@@ -48,6 +88,19 @@ def features2mask(features, image_shape):
48
88
  return segmentation_mask
49
89
 
50
90
 
91
+ def features2instance_mask(features, image_shape):
92
+ segmentation_mask = np.zeros(image_shape, dtype=np.uint16)
93
+ 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)
98
+ feature_properites = feature.get("properties")
99
+ feature_id = feature_properites.get("Detection ID")
100
+ segmentation_mask[feature_mask] = feature_id
101
+ return segmentation_mask
102
+
103
+
51
104
  def boxes2features(boxes: np.ndarray) -> List[Feature]:
52
105
  """
53
106
  Convert an array of bounding boxes to a list of geojson-like features.
@@ -72,6 +72,10 @@ def serialize_result_tuple(result_data_tuple: List[Tuple]) -> List[Dict]:
72
72
  data = data.astype(np.uint16)
73
73
  features = serverkit.mask2features(data)
74
74
  data_params['image_shape'] = data.shape
75
+ elif data_type == "instance_mask":
76
+ data = data.astype(np.uint16)
77
+ features = serverkit.instance_mask2features(data)
78
+ data_params['image_shape'] = data.shape
75
79
  elif data_type == "labels3d":
76
80
  features = serverkit.encode_contents(data.astype(np.uint16))
77
81
  elif data_type == "points":
@@ -85,8 +89,13 @@ def serialize_result_tuple(result_data_tuple: List[Tuple]) -> List[Dict]:
85
89
  features = serverkit.vectors2features(data)
86
90
  elif data_type == "tracks":
87
91
  features = serverkit.encode_contents(data.astype(np.float32))
92
+ elif data_type == "class":
93
+ features = data # A simple string
94
+ elif data_type == "text":
95
+ features = data # A text string
88
96
  else:
89
97
  print(f"Unknown data_type: {data_type}")
98
+ features = None
90
99
 
91
100
  serialized_results.append(
92
101
  {
@@ -114,6 +123,9 @@ def deserialize_result_tuple(serialized_results: List[Dict]) -> List[Tuple]:
114
123
  elif data_type == "labels":
115
124
  image_shape = data_params.pop('image_shape')
116
125
  data = serverkit.features2mask(features, image_shape)
126
+ elif data_type == "instance_mask":
127
+ image_shape = data_params.pop('image_shape')
128
+ data = serverkit.features2instance_mask(features, image_shape)
117
129
  elif data_type == "labels3d":
118
130
  data = serverkit.decode_contents(features).astype(int)
119
131
  elif data_type == "points":
@@ -126,6 +138,10 @@ def deserialize_result_tuple(serialized_results: List[Dict]) -> List[Tuple]:
126
138
  data = serverkit.features2vectors(features)
127
139
  elif data_type == "tracks":
128
140
  data = serverkit.decode_contents(features).astype(float)
141
+ elif data_type == "class":
142
+ data = features # A simple string
143
+ elif data_type == "text":
144
+ data = features # A text string
129
145
  else:
130
146
  print(f"Unknown data_type: {data_type}")
131
147
 
@@ -45,10 +45,20 @@ class Parameters(BaseModel):
45
45
 
46
46
 
47
47
  class Server:
48
- def __init__(self, algorithm_name: str, parameters_model: Type[BaseModel]):
48
+ def __init__(
49
+ self,
50
+ algorithm_name: str,
51
+ parameters_model: Type[BaseModel],
52
+ metadata_file: str = None,
53
+ ):
49
54
  self.algorithm_name = algorithm_name
50
55
  self.parameters_model = parameters_model
51
56
 
57
+ if metadata_file is None:
58
+ self.metadata_file = "metadata.yaml"
59
+ else:
60
+ self.metadata_file = metadata_file
61
+
52
62
  self.app = FastAPI(title=algorithm_name, lifespan=self.lifespan)
53
63
  self.app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
54
64
 
@@ -117,7 +127,7 @@ class Server:
117
127
 
118
128
  @self.app.get(f"/{self.algorithm_name}/info", response_class=HTMLResponse)
119
129
  def info(request: Request):
120
- algo_info = load_from_yaml("metadata.yaml")
130
+ algo_info = load_from_yaml(self.metadata_file)
121
131
  algo_params_schema = get_algo_params()
122
132
  algo_params = parse_algo_params_schema(algo_params_schema)
123
133
  return templates.TemplateResponse(
@@ -129,19 +139,30 @@ class Server:
129
139
  },
130
140
  )
131
141
 
132
- @self.app.post(f"/{self.algorithm_name}/process", status_code=status.HTTP_201_CREATED)
142
+ @self.app.post(
143
+ f"/{self.algorithm_name}/process", status_code=status.HTTP_201_CREATED
144
+ )
133
145
  async def run_algo(algo_params: self.parameters_model):
134
146
  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.")
147
+ result_data_tuple = await asyncio.wait_for(
148
+ self._run_algorithm(**algo_params.dict()), timeout=600
149
+ )
150
+ except (
151
+ asyncio.TimeoutError
152
+ ): # This works but doesn't actually kill the process... we need something else to terminate it
153
+ raise HTTPException(
154
+ status_code=504, detail="Request timeout during processing."
155
+ )
138
156
  try:
139
- serialized_results = await asyncio.wait_for(self._serialize_result_tuple(result_data_tuple), timeout=10)
157
+ serialized_results = await asyncio.wait_for(
158
+ self._serialize_result_tuple(result_data_tuple), timeout=600
159
+ )
140
160
  except asyncio.TimeoutError:
141
- raise HTTPException(status_code=504, detail="Request timeout during serialization.")
142
-
143
- return serialized_results
161
+ raise HTTPException(
162
+ status_code=504, detail="Request timeout during serialization."
163
+ )
144
164
 
165
+ return serialized_results
145
166
 
146
167
  @self.app.get(f"/{self.algorithm_name}/parameters", response_model=dict)
147
168
  def get_algo_params():
@@ -156,15 +177,17 @@ class Server:
156
177
  return {"sample_images": encoded_images}
157
178
 
158
179
  async def _serialize_result_tuple(self, result_data_tuple):
159
- serialized_results = await asyncio.to_thread(serverkit.serialize_result_tuple, result_data_tuple)
180
+ serialized_results = await asyncio.to_thread(
181
+ serverkit.serialize_result_tuple, result_data_tuple
182
+ )
160
183
  return serialized_results
161
-
184
+
162
185
  async def _run_algorithm(self, **algo_params):
163
186
  result_data_tuple = await asyncio.to_thread(self.run_algorithm, **algo_params)
164
187
  return result_data_tuple
165
-
188
+
166
189
  def load_sample_images(self) -> List["np.ndarray"]:
167
190
  raise NotImplementedError("Subclasses should implement this method")
168
-
191
+
169
192
  def run_algorithm(self, **algo_params) -> List[Tuple]:
170
193
  raise NotImplementedError("Subclasses should implement this method")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: imaging-server-kit
3
- Version: 0.0.4
3
+ Version: 0.0.5
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
@@ -31,8 +31,8 @@ License: Copyright (c) 2024, EPFL
31
31
  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32
32
  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
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
34
+ Project-URL: homepage, https://github.com/Imaging-Server-Kit/imaging-server-kit
35
+ Project-URL: repository, https://github.com/Imaging-Server-Kit/imaging-server-kit
36
36
  Classifier: Development Status :: 2 - Pre-Alpha
37
37
  Classifier: Intended Audience :: Developers
38
38
  Classifier: License :: OSI Approved :: BSD License
@@ -53,6 +53,8 @@ Requires-Dist: python-multipart
53
53
  Requires-Dist: tifffile
54
54
  Requires-Dist: numpy
55
55
  Requires-Dist: scikit-image
56
+ Requires-Dist: opencv-contrib-python-headless
57
+ Requires-Dist: imantics
56
58
  Requires-Dist: geojson
57
59
  Requires-Dist: jinja2
58
60
  Requires-Dist: pyyaml
@@ -90,9 +92,10 @@ All of these algorithm servers can be installed locally or built and run with do
90
92
  ```{docker-compose.yml}
91
93
  services:
92
94
  servers_registry:
93
- image: mallorywittwerepfl/imaging-server-kit:registry
95
+ image: mallorywittwerepfl/imaging-server-kit:latest
94
96
  ports:
95
97
  - "8000:8000"
98
+ command: python3 start_registry.py
96
99
  rembg:
97
100
  image: registry.rcp.epfl.ch/imaging-server-kit/serverkit-rembg:latest
98
101
  depends_on:
@@ -4,6 +4,8 @@ python-multipart
4
4
  tifffile
5
5
  numpy
6
6
  scikit-image
7
+ opencv-contrib-python-headless
8
+ imantics
7
9
  geojson
8
10
  jinja2
9
11
  pyyaml