dsa-helpers 0.3.dev0__tar.gz → 0.3.dev1__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 (26) hide show
  1. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/.gitignore +3 -1
  2. {dsa_helpers-0.3.dev0/dsa_helpers.egg-info → dsa_helpers-0.3.dev1}/PKG-INFO +10 -6
  3. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/__init__.py +1 -1
  4. dsa_helpers-0.3.dev1/dsa_helpers/dash/__init__.py +1 -0
  5. dsa_helpers-0.3.dev1/dsa_helpers/dash/header.py +141 -0
  6. dsa_helpers-0.3.dev1/dsa_helpers/dash/header_callbacks.py +98 -0
  7. dsa_helpers-0.3.dev1/dsa_helpers/girder_utils.py +287 -0
  8. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/login.py +3 -1
  9. dsa_helpers-0.3.dev1/dsa_helpers/mongo_utils.py +147 -0
  10. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1/dsa_helpers.egg-info}/PKG-INFO +10 -6
  11. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/SOURCES.txt +7 -1
  12. dsa_helpers-0.3.dev1/dsa_helpers.egg-info/requires.txt +9 -0
  13. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/pyproject.toml +10 -6
  14. dsa_helpers-0.3.dev1/readthedocs.yaml +27 -0
  15. dsa_helpers-0.3.dev1/requirements.txt +10 -0
  16. dsa_helpers-0.3.dev0/dsa_helpers/girder_utils.py +0 -116
  17. dsa_helpers-0.3.dev0/dsa_helpers.egg-info/requires.txt +0 -5
  18. dsa_helpers-0.3.dev0/requirements.txt +0 -5
  19. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/LICENSE +0 -0
  20. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/README.md +0 -0
  21. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/image_utils.py +0 -0
  22. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/imread.py +0 -0
  23. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/imwrite.py +0 -0
  24. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/dependency_links.txt +0 -0
  25. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/top_level.txt +0 -0
  26. {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/setup.cfg +0 -0
@@ -5,4 +5,6 @@ pyenv/
5
5
  # Do not push the version file.
6
6
  dsa_utils/_version.py
7
7
 
8
- *.pyc
8
+ *.pyc
9
+ *.egg-info
10
+ dist/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dsa_helpers
3
- Version: 0.3.dev0
3
+ Version: 0.3.dev1
4
4
  Summary: Utility functions for working with the DSA girder client.
5
5
  Author-email: "Juan C. Vizcarra" <jvizcar@emory.edu>
6
6
  Maintainer-email: "Juan C. Vizcarra" <jvizcar@emory.edu>
@@ -11,11 +11,15 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: girder-client==3.2.3
15
- Requires-Dist: numpy==2.0.0
16
- Requires-Dist: setuptools==71.0.3
17
- Requires-Dist: opencv-python==4.10.0.84
18
- Requires-Dist: pandas==2.2.2
14
+ Requires-Dist: girder-client
15
+ Requires-Dist: numpy
16
+ Requires-Dist: setuptools
17
+ Requires-Dist: opencv-python
18
+ Requires-Dist: pandas
19
+ Requires-Dist: pymongo
20
+ Requires-Dist: pillow
21
+ Requires-Dist: dash
22
+ Requires-Dist: dash_bootstrap_components
19
23
 
20
24
  # Digital Slide Archive (DSA) Helpers
21
25
  Digital Slide Archive Python utility library.
@@ -4,4 +4,4 @@ from .imread import imread
4
4
  from .imwrite import imwrite
5
5
 
6
6
  # Modules that should be available.
7
- from . import girder_utils, image_utils
7
+ from . import girder_utils, image_utils, dash
@@ -0,0 +1 @@
1
+ from . import header
@@ -0,0 +1,141 @@
1
+ from dash import html, dcc
2
+ import dash_bootstrap_components as dbc
3
+ from . import header_callbacks
4
+
5
+
6
+ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
7
+ """Get the Dash html component that contains the header of the application
8
+ with login capabilities.
9
+
10
+ Args:
11
+ title (str): The title of the application.
12
+ config (dict): The configuration of the component.
13
+
14
+ backgroundColor
15
+ titleColor
16
+ fontColor
17
+
18
+ Returns:
19
+ html.Div: The header component.
20
+
21
+ """
22
+ login_modal = dbc.Modal(
23
+ [
24
+ dbc.ModalHeader("Log in"),
25
+ dbc.ModalBody(
26
+ [
27
+ html.Div(
28
+ "Login or email", style={"margin": 5, "fontWeight": "bold"}
29
+ ),
30
+ dbc.Input(
31
+ id="login",
32
+ type="text",
33
+ placeholder="Enter login",
34
+ style={"margin": 5},
35
+ ),
36
+ html.Div(
37
+ "Password",
38
+ style={"margin": 5, "marginTop": 15, "fontWeight": "bold"},
39
+ ),
40
+ dbc.Input(
41
+ id="password",
42
+ type="password",
43
+ placeholder="Enter password",
44
+ style={"margin": 5},
45
+ ),
46
+ html.Div(
47
+ "Login failed.",
48
+ hidden=True,
49
+ id="login-failed",
50
+ style={"color": "red", "fontWeight": "bold", "margin": 10},
51
+ ),
52
+ ],
53
+ ),
54
+ dbc.ModalFooter(
55
+ dbc.Row(
56
+ [
57
+ dbc.Col(
58
+ dbc.Button(
59
+ "Close",
60
+ id="close-login-modal",
61
+ className="me-1",
62
+ color="light",
63
+ )
64
+ ),
65
+ dbc.Col(
66
+ dbc.Button(
67
+ "Login",
68
+ id="log-in-btn",
69
+ className="me-1",
70
+ color="primary",
71
+ )
72
+ ),
73
+ ],
74
+ )
75
+ ),
76
+ ],
77
+ is_open=False,
78
+ id="login-modal",
79
+ )
80
+
81
+ logout_modal = dbc.Modal(
82
+ [
83
+ dbc.ModalFooter(
84
+ [
85
+ dbc.Button(
86
+ "Log out",
87
+ id="logout-btn",
88
+ color="danger",
89
+ className="me-1",
90
+ ),
91
+ ]
92
+ )
93
+ ],
94
+ is_open=False,
95
+ id="logout-modal",
96
+ )
97
+
98
+ if config is None:
99
+ config = {}
100
+
101
+ return html.Div(
102
+ [
103
+ dbc.Row(
104
+ [
105
+ dbc.Col(
106
+ html.H1(
107
+ title,
108
+ style={
109
+ "fontWeight": "bold",
110
+ "color": config.get("titleColor", "#d9d9d6"),
111
+ "marginLeft": 5,
112
+ },
113
+ ),
114
+ width="auto",
115
+ ),
116
+ dbc.Col(
117
+ dbc.Button(
118
+ "Log in",
119
+ id="login-btn",
120
+ color=config.get("backgroundColor", "#012169"),
121
+ style={
122
+ "color": config.get("fontColor", "#f2a900"),
123
+ "fontSize": "1.2rem",
124
+ },
125
+ className="me-1",
126
+ ),
127
+ width="auto",
128
+ ),
129
+ ],
130
+ justify="between",
131
+ align="center",
132
+ ),
133
+ login_modal,
134
+ logout_modal,
135
+ dcc.Store(id="user-store", storage_type="local", data={}),
136
+ ],
137
+ style={
138
+ "backgroundColor": config.get("backgroundColor", "#012169"),
139
+ "padding": 5,
140
+ },
141
+ )
@@ -0,0 +1,98 @@
1
+ # Callbacks for the header component.
2
+ from dash import callback, Output, Input, State, no_update
3
+ from girder_client import GirderClient
4
+ from os import getenv
5
+
6
+
7
+ @callback(Output("login-btn", "children"), Input("user-store", "data"))
8
+ def check_user_store(data):
9
+ # Check if the user store has user info or if no one is logged in.
10
+ return data["user"] if len(data) else "Log in"
11
+
12
+
13
+ @callback(
14
+ [
15
+ Output("login-modal", "is_open", allow_duplicate=True),
16
+ Output("logout-modal", "is_open", allow_duplicate=True),
17
+ ],
18
+ [Input("login-btn", "n_clicks"), State("login-btn", "children")],
19
+ prevent_initial_call=True,
20
+ )
21
+ def open_login_modal(n_clicks, children):
22
+ # Open login / logout modal.
23
+ if n_clicks:
24
+ if children == "Log in":
25
+ return True, False
26
+ else:
27
+ return False, True
28
+
29
+ return False, False
30
+
31
+
32
+ @callback(
33
+ [
34
+ Output("user-store", "data"),
35
+ Output("login-failed", "hidden", allow_duplicate=True),
36
+ Output("login-modal", "is_open", allow_duplicate=True),
37
+ Output("login", "value", allow_duplicate=True),
38
+ Output("password", "value", allow_duplicate=True),
39
+ ],
40
+ [
41
+ Input("log-in-btn", "n_clicks"),
42
+ State("login", "value"),
43
+ State("password", "value"),
44
+ ],
45
+ prevent_initial_call=True,
46
+ )
47
+ def login(n_clicks, login, password):
48
+ # Try to login.
49
+ gc = GirderClient(apiUrl=getenv("DSA_API_URL"))
50
+
51
+ try:
52
+ _ = gc.authenticate(username=login, password=password)
53
+
54
+ response = gc.get("token/session")
55
+
56
+ user = gc.get("user/me")["login"]
57
+
58
+ return {"user": user, "token": response["token"]}, True, False, "", ""
59
+ except:
60
+ return (
61
+ {},
62
+ False,
63
+ True,
64
+ no_update,
65
+ no_update,
66
+ )
67
+
68
+
69
+ @callback(
70
+ [
71
+ Output("login-modal", "is_open", allow_duplicate=True),
72
+ Output("login", "value", allow_duplicate=True),
73
+ Output("password", "value", allow_duplicate=True),
74
+ Output("login-failed", "hidden", allow_duplicate=True),
75
+ ],
76
+ Input("close-login-modal", "n_clicks"),
77
+ prevent_initial_call=True,
78
+ )
79
+ def close_login_modal(n_clicks):
80
+ if n_clicks:
81
+ return False, "", "", True
82
+
83
+ return False, "", "", True
84
+
85
+
86
+ @callback(
87
+ [
88
+ Output("user-store", "data", allow_duplicate=True),
89
+ Output("logout-modal", "is_open", allow_duplicate=True),
90
+ ],
91
+ Input("logout-btn", "n_clicks"),
92
+ prevent_initial_call=True,
93
+ )
94
+ def logout(n_clicks):
95
+ if n_clicks:
96
+ return {}, False
97
+
98
+ return no_update, False
@@ -0,0 +1,287 @@
1
+ # Functions using the girder client.
2
+ from girder_client import GirderClient
3
+ import pickle
4
+ import numpy as np
5
+ import pandas as pd
6
+ from pathlib import Path
7
+ import cv2 as cv
8
+ from copy import deepcopy
9
+
10
+
11
+ def get_item_large_image_metadata(gc: GirderClient, item_id: str) -> dict:
12
+ """Get large image metadata for an item.
13
+
14
+ Args:
15
+ gc (girder_client.GirderClient): The authenticated girder client.
16
+ item_id (str): The item id.
17
+
18
+ Returns:
19
+ dict: The metadata of the large image item.
20
+
21
+ """
22
+ return gc.get(f"item/{item_id}/tiles")
23
+
24
+
25
+ def get_thumbnail(
26
+ gc: GirderClient,
27
+ item_id: str,
28
+ mag: float | None = None,
29
+ width: int | None = None,
30
+ height: int | None = None,
31
+ fill: int | tuple = (255, 255, 255),
32
+ ) -> np.ndarray:
33
+ """Get the thumbnail image by a specific magnification or shape. If mag is
34
+ not None, then width and height are ignored. Fill is only used when both
35
+ width and height are provided, to return the thumbnail at the exact shape.
36
+ DSA convention will fill the height of the image, centering the image and
37
+ filling the top and bottom of the image equally.
38
+
39
+ Args:
40
+ gc (girder_client.GirderClient): The authenticated girder client.
41
+ item_id (str): The item id.
42
+ mag (float, optional): The magnification. Defaults to None.
43
+ width (int, optional): The width of the thumbnail. Defaults to None.
44
+ height (int, optional): The height of the thumbnail. Defaults to None.
45
+ fill (int | tuple, optional): The fill color. Defaults to (255, 255, 255).
46
+
47
+ Returns:
48
+ np.ndarray: The thumbnail image.
49
+
50
+ """
51
+ get_url = f"item/{item_id}/tiles/"
52
+
53
+ if mag is not None:
54
+ get_url += f"region?magnification={mag}&encoding=pickle"
55
+ else:
56
+ # Instead use width and height.
57
+ params = ["encoding=pickle"]
58
+
59
+ if width is not None and height is not None:
60
+ if isinstance(fill, (tuple, list)):
61
+ if len(fill) == 3:
62
+ fill = f"rgb({fill[0]},{fill[1]},{fill[2]})"
63
+ elif len(fill) == 4:
64
+ fill = f"rgba({fill[0]},{fill[1]},{fill[2]},{fill[3]})"
65
+
66
+ params.extend([f"width={width}", f"height={height}", f"fill={fill}"])
67
+ elif width is not None:
68
+ params.append(f"width={width}")
69
+ elif height is not None:
70
+ params.append(f"height={height}")
71
+
72
+ get_url += "thumbnail?" + "&".join(params)
73
+
74
+ response = gc.get(get_url, jsonResp=False)
75
+
76
+ return pickle.loads(response.content)
77
+
78
+
79
+ def get_region(
80
+ gc: GirderClient,
81
+ item_id: str,
82
+ left: int,
83
+ top: int,
84
+ width: int,
85
+ height: int,
86
+ mag: float | None = None,
87
+ ) -> np.ndarray:
88
+ """Get a region of the image for an item. Note that the output image might
89
+ not be in the shape (width, height) if left + width or top + height exceeds
90
+ the image size. The outpuzerositem id.
91
+ left (int): The left coordinate.
92
+ top (int): The top coordinate.
93
+ width (int): The width of the region.
94
+ height (int): The height of the region.
95
+ mag (float, optional): The magnification. Defaults to None which returns
96
+ the image at scan magnification. Using a mag lower than the scan
97
+ magnification will result in an ouptut image smaller than the
98
+ width and height. Similarly, using a mag higher than the scan
99
+ magnification will result in an output image larger than the width
100
+ and height.
101
+
102
+ Returns:
103
+ np.ndarray: The region of the image.
104
+
105
+ """
106
+ get_url = (
107
+ f"item/{item_id}/tiles/region?left={left}&top={top}&regionWidth="
108
+ f"{width}&regionHeight={height}&encoding=pickle"
109
+ )
110
+
111
+ if mag is not None:
112
+ get_url += f"&magnification={mag}"
113
+
114
+ response = gc.get(get_url, jsonResp=False)
115
+
116
+ return pickle.loads(response.content)
117
+
118
+
119
+ def get_element_contours(element: dict) -> np.ndarray:
120
+ """Get the contours of an element, regardless of the type.
121
+
122
+ Args:
123
+ element (dict): The element dictionary.
124
+
125
+ Returns:
126
+ np.ndarray: The contours of the element.
127
+
128
+ """
129
+ if element["type"] == "rectangle":
130
+ return get_rectangle_element_coords(element)
131
+ else:
132
+ return None
133
+
134
+
135
+ def get_roi_images(
136
+ gc: GirderClient,
137
+ item_id: str,
138
+ save_dir: str,
139
+ roi_groups: str | list,
140
+ doc_names: str | list | None = None,
141
+ mag: float | None = None,
142
+ rgb_pad: tuple[int, int, int] | None = None,
143
+ ) -> pd.DataFrame:
144
+ """Gets regions of interest (ROIs) as images from DSA annotations.
145
+
146
+ Args:
147
+ gc (girder_client.GirderClient): The authenticated girder client.
148
+ item_id (str): The item id.
149
+ save_dir (str): The directory to save the roi images.
150
+ roi_groups (str | list): The roi group name or list of roi group names.
151
+ doc_names (str | list, optional): List of documents to look for, if None
152
+ then it looks at all documents. Defaults to None.
153
+ mag (float, optional): The magnification to get the roi images. Defaults
154
+ to None which returns the images at scan magnification.
155
+ rgb_pad (tuple[int, int, int], optional): The RGB values to pad the image with.
156
+ This only is used when the annotation is a rotated rectangle or a polygon.
157
+
158
+ Returns:
159
+ pd.DataFrame: The roi images metadata.
160
+
161
+ """
162
+ if isinstance(roi_groups, str):
163
+ roi_groups = [roi_groups]
164
+
165
+ if isinstance(doc_names, str):
166
+ doc_names = [doc_names]
167
+
168
+ # Convert to a comma separated string.
169
+ roi_groups_str = ",".join(roi_groups)
170
+
171
+ if doc_names is None:
172
+ docs = gc.get(
173
+ f"annotation?itemId={item_id}&text="
174
+ f"{roi_groups_str}&limit=0&offset=0&sort=lowerName&sortdir=1"
175
+ )
176
+ else:
177
+ docs = []
178
+
179
+ for doc_name in doc_names:
180
+ docs.extend(
181
+ gc.get(
182
+ f"annotation?itemId={item_id}&text={roi_groups_str}&name={doc_name}&limit=0&offset=0&sort=lowerName&sortdir=1"
183
+ )
184
+ )
185
+
186
+ # Create the location to save the images.
187
+ save_dir = Path(save_dir)
188
+ save_dir.mkdir(parents=True, exist_ok=True)
189
+
190
+ # Loop through each document.
191
+ for doc in docs:
192
+ # Get the full annotations.
193
+ doc = gc.get(f"annotation/{doc['_id']}")
194
+
195
+ # Get the elements.
196
+ for element in doc.get("annotation", {}).get("elements", []):
197
+ if element["group"] in roi_groups:
198
+ # The annotations need to be handled based on type.
199
+ contour = get_element_contours(element)
200
+
201
+ # Get the minimum and maximum coordinates.
202
+ xmin, ymin = contour.min(axis=0)
203
+ xmax, ymax = contour.max(axis=0)
204
+ w, h = xmax - xmin, ymax - ymin
205
+
206
+ # Get the region of interest from the image.
207
+ roi_image = get_region(gc, item_id, xmin, ymin, w, h, mag=mag)[
208
+ :, :, :3
209
+ ] # for now assuming images are RGB
210
+
211
+ # Use pad if needed.
212
+ if rgb_pad is not None:
213
+ roi_mask = np.ones((h, w), dtype=np.uint8)
214
+
215
+ # Draw the contours.
216
+ roi_mask = cv.drawContours(
217
+ roi_mask, [contour - (xmin, ymin)], -1, 0, cv.FILLED
218
+ )
219
+
220
+ roi_image[roi_mask == 1] = rgb_pad
221
+
222
+ return roi_image, contour - (xmin, ymin)
223
+
224
+
225
+ def _rotate_point_list(point_list, rotation, center=(0, 0)):
226
+ """Rotate a list of x, y points around a center location.
227
+ Adapted from: https://github.com/DigitalSlideArchive/HistomicsTK/blob/master/histomicstk/annotations_and_masks/annotation_and_mask_utils.py
228
+ INPUTS
229
+ ------
230
+ point_list : list
231
+ list of x, y coordinates
232
+ rotation : int or float
233
+ rotation in radians
234
+ center : list
235
+ x, y location of center of rotation
236
+ RETURN
237
+ ------
238
+ point_list_rotated : list
239
+ list of x, y coordinates after rotation around center
240
+ """
241
+ point_list_rotated = []
242
+
243
+ for point in point_list:
244
+ cos, sin = np.cos(rotation), np.sin(rotation)
245
+ x = point[0] - center[0]
246
+ y = point[1] - center[1]
247
+
248
+ point_list_rotated.append(
249
+ (int(x * cos - y * sin + center[0]), int(x * sin + y * cos + center[1]))
250
+ )
251
+
252
+ return point_list_rotated
253
+
254
+
255
+ def get_rectangle_element_coords(element):
256
+ """Get the corner coordinate from a rectangle HistomicsUI element, can handle rotated elements.
257
+ Adapted from: https://github.com/DigitalSlideArchive/HistomicsTK/blob/master/histomicstk/annotations_and_masks/annotation_and_mask_utils.py
258
+ INPUTS
259
+ ------
260
+ element : dict
261
+ rectangle element, in HistomicsUI format
262
+ RETURN
263
+ ------
264
+ corner_coords : array
265
+ array of shape [4, 2] for the four corners of the rectangle in (x, y) format
266
+ """
267
+ # element is a dict so prevent referencing
268
+ element = deepcopy(element)
269
+
270
+ # calculate the corner coordinates, assuming no rotation
271
+ center_x, center_y = element["center"][:2]
272
+ h, w = element["height"], element["width"]
273
+ x_min = center_x - w // 2
274
+ x_max = center_x + w // 2
275
+ y_min = center_y - h // 2
276
+ y_max = center_y + h // 2
277
+ corner_coords = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
278
+
279
+ # if there is rotation rotate
280
+ if element["rotation"]:
281
+ corner_coords = _rotate_point_list(
282
+ corner_coords, rotation=element["rotation"], center=(center_x, center_y)
283
+ )
284
+
285
+ corner_coords = np.array(corner_coords, dtype=np.int32)
286
+
287
+ return corner_coords
@@ -24,7 +24,9 @@ def login(
24
24
  gc = GirderClient(apiUrl=api_url)
25
25
 
26
26
  if api_key is None:
27
- if password is None:
27
+ if login_or_email is None:
28
+ _ = gc.authenticate(interactive=True)
29
+ elif password is None:
28
30
  _ = gc.authenticate(username=login_or_email, interactive=True)
29
31
  else:
30
32
  _ = gc.authenticate(username=login_or_email, password=password)
@@ -0,0 +1,147 @@
1
+ import pymongo
2
+ from os import getenv
3
+ from gridfs import GridFS
4
+ import numpy as np
5
+ from io import BytesIO
6
+ from bson.objectid import ObjectId
7
+ from PIL import Image
8
+ from typing import Union
9
+
10
+
11
+ def get_mongo_database(mongo_url: str | None = None, database: str | None = None) -> pymongo.database.Database:
12
+ """Get a mongo database. By default the Mongo URL will be assumed from local
13
+ environment variables:
14
+ "mongodb://$MONGO_INITDB_ROOT_USERNAME:$MONGO_INITDB_ROOT_PASSWORD@mongodb:27017"
15
+
16
+ and the database will be assumed from the environment variable "MONGODB_DB".
17
+
18
+ Args:
19
+ mongo_url (str): The mongo URL.
20
+ database (str): The database name.
21
+
22
+ Returns:
23
+ pymongo.database.Database: The mongo database.
24
+
25
+ """
26
+ if mongo_url is None:
27
+ mongo_url = f"mongodb://{getenv('MONGO_INITDB_ROOT_USERNAME')}:" + \
28
+ f"{getenv('MONGO_INITDB_ROOT_PASSWORD')}" + \
29
+ f"@{getenv('MONGO_HOST_NAME')}:{getenv("MONGO_HOST_PORT")}"
30
+
31
+ mc = pymongo.MongoClient(mongo_url)
32
+
33
+ if database is None:
34
+ database = getenv("MONGODB_DB")
35
+
36
+ # Return the specific database.
37
+ return mc[database]
38
+
39
+
40
+ def chunks(lst: list, n=500):
41
+ """Helper function for traversting through a list in chunks.
42
+
43
+ Args:
44
+ lst (list): The list to traverse.
45
+ n (int): The size of the chunks.
46
+
47
+ Returns:
48
+ generator: A generator of the list in chunks.
49
+
50
+ """
51
+ for i in range(0, len(lst), n):
52
+ yield lst[i : i + n]
53
+
54
+
55
+ def add_many_to_collection(
56
+ mongo_collection: pymongo.collection.Collection,
57
+ items: dict | list, key: str = "_id"
58
+ ) -> dict[str | dict]:
59
+ """Add items to a mongo collection. For this project we always add items with a unique
60
+ user key.
61
+
62
+ Args:
63
+ mongo_collection (pymongo.collection.Collection): The mongo collection to add to.
64
+ items (dict | list): The items to add to the collection.
65
+ key (str): The key to use to identify the items.
66
+
67
+ Returns:
68
+ dict[str | dict]: The items added to the collection.
69
+
70
+ """
71
+ # If the items are a list, then use the "key" parameter to nest it as a dict.
72
+ if isinstance(items, list):
73
+ items = {item[key]: item for item in items}
74
+
75
+ operations = []
76
+
77
+ for item in items:
78
+ operations.append(
79
+ pymongo.UpdateOne({"_id": item[key]}, {"$set": item}, upsert=True)
80
+ )
81
+
82
+ for chunk in chunks(operations):
83
+ _ = mongo_collection.bulk_write(chunk)
84
+
85
+ return items
86
+
87
+
88
+ def get_img_from_db(mongo_db: pymongo.database.Database, img_id: str) -> Union[np.ndarray, None]:
89
+ """Get an image from the database by its location id.
90
+
91
+ Args:
92
+ mongo_db (pymongo.database.Database): The mongo database.
93
+ img_loc (str): The image id in mongo.
94
+
95
+ Returns:
96
+ np.array: The image, if None then the image was not found.
97
+
98
+ """
99
+ fs = GridFS(mongo_db)
100
+
101
+ grid_out = fs.get(ObjectId(img_id))
102
+
103
+ if grid_out:
104
+ # Read the byte data from the GridOut object
105
+ byte_data = grid_out.read()
106
+
107
+ # Create a BytesIO object from the byte data
108
+ byte_io = BytesIO(byte_data)
109
+
110
+ # Open the image from the BytesIO object
111
+ src_img = np.array(Image.open(byte_io))
112
+
113
+ return src_img
114
+
115
+
116
+ def add_img_to_db(
117
+ mongo_db: pymongo.database.Database,
118
+ img: Image.Image | np.ndarray,
119
+ ) -> str:
120
+ """Add an image to the database.
121
+
122
+ Args:
123
+ mongo_db (pymongo.database.Database): The mongo database.
124
+ img (np.ndarray or PIL.Image.Image): The image.
125
+
126
+ Returns:
127
+ str: The image id in the database.
128
+
129
+ """
130
+ fs = GridFS(mongo_db)
131
+
132
+ # Convert image to PIL image if needed.
133
+ if isinstance(img, np.ndarray):
134
+ img = Image.fromarray(img)
135
+
136
+ # Create a BytesIO object and save the image to it
137
+ byte_io = BytesIO()
138
+ img.save(byte_io, "PNG")
139
+
140
+ # Get the byte data from the BytesIO object
141
+ byte_io.seek(0)
142
+ byte_data = byte_io.read()
143
+
144
+ # Save the byte data to MongoDB using GridFS
145
+ file_id = fs.put(byte_data)
146
+
147
+ return file_id
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dsa_helpers
3
- Version: 0.3.dev0
3
+ Version: 0.3.dev1
4
4
  Summary: Utility functions for working with the DSA girder client.
5
5
  Author-email: "Juan C. Vizcarra" <jvizcar@emory.edu>
6
6
  Maintainer-email: "Juan C. Vizcarra" <jvizcar@emory.edu>
@@ -11,11 +11,15 @@ Classifier: License :: OSI Approved :: MIT License
11
11
  Requires-Python: >=3.9
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: girder-client==3.2.3
15
- Requires-Dist: numpy==2.0.0
16
- Requires-Dist: setuptools==71.0.3
17
- Requires-Dist: opencv-python==4.10.0.84
18
- Requires-Dist: pandas==2.2.2
14
+ Requires-Dist: girder-client
15
+ Requires-Dist: numpy
16
+ Requires-Dist: setuptools
17
+ Requires-Dist: opencv-python
18
+ Requires-Dist: pandas
19
+ Requires-Dist: pymongo
20
+ Requires-Dist: pillow
21
+ Requires-Dist: dash
22
+ Requires-Dist: dash_bootstrap_components
19
23
 
20
24
  # Digital Slide Archive (DSA) Helpers
21
25
  Digital Slide Archive Python utility library.
@@ -2,6 +2,7 @@
2
2
  LICENSE
3
3
  README.md
4
4
  pyproject.toml
5
+ readthedocs.yaml
5
6
  requirements.txt
6
7
  dsa_helpers/__init__.py
7
8
  dsa_helpers/girder_utils.py
@@ -9,8 +10,13 @@ dsa_helpers/image_utils.py
9
10
  dsa_helpers/imread.py
10
11
  dsa_helpers/imwrite.py
11
12
  dsa_helpers/login.py
13
+ dsa_helpers/mongo_utils.py
14
+ dsa_helpers/object_detection.py
12
15
  dsa_helpers.egg-info/PKG-INFO
13
16
  dsa_helpers.egg-info/SOURCES.txt
14
17
  dsa_helpers.egg-info/dependency_links.txt
15
18
  dsa_helpers.egg-info/requires.txt
16
- dsa_helpers.egg-info/top_level.txt
19
+ dsa_helpers.egg-info/top_level.txt
20
+ dsa_helpers/dash/__init__.py
21
+ dsa_helpers/dash/header.py
22
+ dsa_helpers/dash/header_callbacks.py
@@ -0,0 +1,9 @@
1
+ girder-client
2
+ numpy
3
+ setuptools
4
+ opencv-python
5
+ pandas
6
+ pymongo
7
+ pillow
8
+ dash
9
+ dash_bootstrap_components
@@ -4,11 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  dependencies = [
7
- 'girder-client==3.2.3',
8
- 'numpy==2.0.0',
9
- 'setuptools==71.0.3',
10
- 'opencv-python==4.10.0.84',
11
- 'pandas==2.2.2'
7
+ 'girder-client',
8
+ 'numpy',
9
+ 'setuptools',
10
+ 'opencv-python',
11
+ 'pandas',
12
+ 'pymongo',
13
+ 'pillow',
14
+ 'dash',
15
+ 'dash_bootstrap_components'
12
16
  ]
13
17
  name = "dsa_helpers"
14
18
  authors = [
@@ -25,7 +29,7 @@ classifiers = [
25
29
  "License :: OSI Approved :: MIT License",
26
30
  ]
27
31
  keywords = ["dsa", "girder", "girder_client"]
28
- version = "v0.3dev"
32
+ version = "v0.3dev1"
29
33
 
30
34
  [project.urls]
31
35
  Repository = "https://github.com/Gutman-Lab/dsa-python-utils"
@@ -0,0 +1,27 @@
1
+ # .readthedocs.yaml
2
+ # Read the Docs configuration file
3
+ # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4
+
5
+ # Required
6
+ version: 2
7
+
8
+ # Set the OS, Python version and other tools you might need
9
+ build:
10
+ tools:
11
+ python: "3.9"
12
+
13
+ # Build documentation in the "docs/" directory with Sphinx
14
+ sphinx:
15
+ configuration: docs/conf.py
16
+
17
+ # Optionally build your docs in additional formats such as PDF and ePub
18
+ # formats:
19
+ # - pdf
20
+ # - epub
21
+
22
+ # Optional but recommended, declare the Python requirements required
23
+ # to build your documentation
24
+ # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
25
+ # python:
26
+ # install:
27
+ # - requirements: docs/requirements.txt
@@ -0,0 +1,10 @@
1
+ girder-client
2
+ numpy
3
+ setuptools
4
+ opencv-python
5
+ pandas
6
+ pymongo
7
+ jupyter
8
+ pillow
9
+ dash
10
+ dash_bootstrap_components
@@ -1,116 +0,0 @@
1
- # Functions using the girder client.
2
- from girder_client import GirderClient
3
- import pickle
4
- import numpy as np
5
-
6
-
7
- def get_item_large_image_metadata(gc: GirderClient, item_id: str) -> dict:
8
- """Get large image metadata for an item.
9
-
10
- Args:
11
- gc (girder_client.GirderClient): The authenticated girder client.
12
- item_id (str): The item id.
13
-
14
- Returns:
15
- dict: The metadata of the large image item.
16
-
17
- """
18
- return gc.get(f"item/{item_id}/tiles")
19
-
20
-
21
- def get_thumbnail(
22
- gc: GirderClient,
23
- item_id: str,
24
- mag: float | None = None,
25
- width: int | None = None,
26
- height: int | None = None,
27
- fill: int | tuple = (255, 255, 255),
28
- ) -> np.ndarray:
29
- """Get the thumbnail image by a specific magnification or shape. If mag is
30
- not None, then width and height are ignored. Fill is only used when both
31
- width and height are provided, to return the thumbnail at the exact shape.
32
- DSA convention will fill the height of the image, centering the image and
33
- filling the top and bottom of the image equally.
34
-
35
- Args:
36
- gc (girder_client.GirderClient): The authenticated girder client.
37
- item_id (str): The item id.
38
- mag (float, optional): The magnification. Defaults to None.
39
- width (int, optional): The width of the thumbnail. Defaults to None.
40
- height (int, optional): The height of the thumbnail. Defaults to None.
41
- fill (int | tuple, optional): The fill color. Defaults to (255, 255, 255).
42
-
43
- Returns:
44
- np.ndarray: The thumbnail image.
45
-
46
- """
47
- get_url = f"item/{item_id}/tiles/"
48
-
49
- if mag is not None:
50
- get_url += f"region?magnification={mag}&encoding=pickle"
51
- else:
52
- # Instead use width and height.
53
- params = ["encoding=pickle"]
54
-
55
- if width is not None and height is not None:
56
- if isinstance(fill, (tuple, list)):
57
- if len(fill) == 3:
58
- fill = f"rgb({fill[0]},{fill[1]},{fill[2]})"
59
- elif len(fill) == 4:
60
- fill = f"rgba({fill[0]},{fill[1]},{fill[2]},{fill[3]})"
61
-
62
- params.extend([f"width={width}", f"height={height}", f"fill={fill}"])
63
- elif width is not None:
64
- params.append(f"width={width}")
65
- elif height is not None:
66
- params.append(f"height={height}")
67
-
68
- get_url += "thumbnail?" + "&".join(params)
69
-
70
- response = gc.get(get_url, jsonResp=False)
71
-
72
- return pickle.loads(response.content)
73
-
74
-
75
- def get_region(
76
- gc: GirderClient,
77
- item_id: str,
78
- left: int,
79
- top: int,
80
- width: int,
81
- height: int,
82
- mag: float | None = None,
83
- ) -> np.ndarray:
84
- """Get a region of the image for an item. Note that the output image might
85
- not be in the shape (width, height) if left + width or top + height exceeds
86
- the image size. The output image will be the maximum size possible.
87
-
88
- Args:
89
- gc (girder_client.GirderClient): The authenticated girder client.
90
- item_id (str): The item id.
91
- left (int): The left coordinate.
92
- top (int): The top coordinate.
93
- width (int): The width of the region.
94
- height (int): The height of the region.
95
- mag (float, optional): The magnification. Defaults to None which returns
96
- the image at scan magnification. Using a mag lower than the scan
97
- magnification will result in an ouptut image smaller than the
98
- width and height. Similarly, using a mag higher than the scan
99
- magnification will result in an output image larger than the width
100
- and height.
101
-
102
- Returns:
103
- np.ndarray: The region of the image.
104
-
105
- """
106
- get_url = (
107
- f"item/{item_id}/tiles/region?left={left}&top={top}&regionWidth="
108
- f"{width}&regionHeight={height}&encoding=pickle"
109
- )
110
-
111
- if mag is not None:
112
- get_url += f"&magnification={mag}"
113
-
114
- response = gc.get(get_url, jsonResp=False)
115
-
116
- return pickle.loads(response.content)
@@ -1,5 +0,0 @@
1
- girder-client==3.2.3
2
- numpy==2.0.0
3
- setuptools==71.0.3
4
- opencv-python==4.10.0.84
5
- pandas==2.2.2
@@ -1,5 +0,0 @@
1
- setuptools==71.0.3
2
- girder-client==3.2.3
3
- numpy==2.0.0
4
- opencv-python==4.10.0.84
5
- pandas==2.2.2
File without changes
File without changes
File without changes