dsa-helpers 0.3.dev0__py3-none-any.whl → 0.3.dev1__py3-none-any.whl
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.
- dsa_helpers/__init__.py +1 -1
- dsa_helpers/dash/__init__.py +1 -0
- dsa_helpers/dash/header.py +141 -0
- dsa_helpers/dash/header_callbacks.py +98 -0
- dsa_helpers/girder_utils.py +176 -5
- dsa_helpers/login.py +3 -1
- dsa_helpers/mongo_utils.py +147 -0
- {dsa_helpers-0.3.dev0.dist-info → dsa_helpers-0.3.dev1.dist-info}/METADATA +10 -6
- dsa_helpers-0.3.dev1.dist-info/RECORD +15 -0
- {dsa_helpers-0.3.dev0.dist-info → dsa_helpers-0.3.dev1.dist-info}/WHEEL +1 -1
- dsa_helpers-0.3.dev0.dist-info/RECORD +0 -11
- {dsa_helpers-0.3.dev0.dist-info → dsa_helpers-0.3.dev1.dist-info}/LICENSE +0 -0
- {dsa_helpers-0.3.dev0.dist-info → dsa_helpers-0.3.dev1.dist-info}/top_level.txt +0 -0
dsa_helpers/__init__.py
CHANGED
|
@@ -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
|
dsa_helpers/girder_utils.py
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
from girder_client import GirderClient
|
|
3
3
|
import pickle
|
|
4
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
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
def get_item_large_image_metadata(gc: GirderClient, item_id: str) -> dict:
|
|
@@ -83,11 +87,7 @@ def get_region(
|
|
|
83
87
|
) -> np.ndarray:
|
|
84
88
|
"""Get a region of the image for an item. Note that the output image might
|
|
85
89
|
not be in the shape (width, height) if left + width or top + height exceeds
|
|
86
|
-
the image size. The
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
gc (girder_client.GirderClient): The authenticated girder client.
|
|
90
|
-
item_id (str): The item id.
|
|
90
|
+
the image size. The outpuzerositem id.
|
|
91
91
|
left (int): The left coordinate.
|
|
92
92
|
top (int): The top coordinate.
|
|
93
93
|
width (int): The width of the region.
|
|
@@ -114,3 +114,174 @@ def get_region(
|
|
|
114
114
|
response = gc.get(get_url, jsonResp=False)
|
|
115
115
|
|
|
116
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
|
dsa_helpers/login.py
CHANGED
|
@@ -24,7 +24,9 @@ def login(
|
|
|
24
24
|
gc = GirderClient(apiUrl=api_url)
|
|
25
25
|
|
|
26
26
|
if api_key is None:
|
|
27
|
-
if
|
|
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.
|
|
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
|
|
15
|
-
Requires-Dist: numpy
|
|
16
|
-
Requires-Dist: setuptools
|
|
17
|
-
Requires-Dist: opencv-python
|
|
18
|
-
Requires-Dist: pandas
|
|
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.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
dsa_helpers/__init__.py,sha256=KEU-DySliRpFR1J992Vp6-z0oj5_pE19mbXBiVgZCYQ,182
|
|
2
|
+
dsa_helpers/girder_utils.py,sha256=5dkgF5-dQZy7CvK7K7r6Rd2AvMxJglVDKERWJgABKlw,9630
|
|
3
|
+
dsa_helpers/image_utils.py,sha256=6VUteE_0MfFoRNHt4E2v7pb8XoSjbBxwdhNCk9Gzmgs,1757
|
|
4
|
+
dsa_helpers/imread.py,sha256=jPr6hO4TOtGphENJQWLLtTaQ377yAwLe4F9jSAehJXQ,885
|
|
5
|
+
dsa_helpers/imwrite.py,sha256=eQI8VGn7M35_85QMPrNqGgS66iQn9YboCS50tigAE8s,1159
|
|
6
|
+
dsa_helpers/login.py,sha256=-QyVLW9fKywiPUQCPWxF1BIsufAOqMizDM062nUbupA,1147
|
|
7
|
+
dsa_helpers/mongo_utils.py,sha256=U962oIESlOnv2T439OtvQO4scWa81Pnml3jp9Y9kc7Q,4121
|
|
8
|
+
dsa_helpers/dash/__init__.py,sha256=tNGwvi7Rgr_cSYT7qbbB3Y0mlBZ0pFEA5WFPXjQpugE,21
|
|
9
|
+
dsa_helpers/dash/header.py,sha256=rV3VWbo_LJeJHuByek69errOk2m0csztNZeE9h5WmIE,4433
|
|
10
|
+
dsa_helpers/dash/header_callbacks.py,sha256=FKUcfS4F5SBKumDbY66GDkoIVmBq1EbmmvDrCm6imvw,2594
|
|
11
|
+
dsa_helpers-0.3.dev1.dist-info/LICENSE,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
|
|
12
|
+
dsa_helpers-0.3.dev1.dist-info/METADATA,sha256=5wxYAX0Ui0pPnFrm70SP9KgzN0xp1z5SbNlNuXdGFQU,1056
|
|
13
|
+
dsa_helpers-0.3.dev1.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
|
|
14
|
+
dsa_helpers-0.3.dev1.dist-info/top_level.txt,sha256=BmAEDVvDa1MJlSUFHWzx73UKec3o1K6dFUd9tHwgHGo,12
|
|
15
|
+
dsa_helpers-0.3.dev1.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
dsa_helpers/__init__.py,sha256=CxfyUmq02ExTQVMFZOUU8WdC5hLm13_DTAbON2SYRSs,176
|
|
2
|
-
dsa_helpers/girder_utils.py,sha256=H9faO5aX1U_UlIi6AvPbHmApooeGsWzX9r1uOeNIUnM,3932
|
|
3
|
-
dsa_helpers/image_utils.py,sha256=6VUteE_0MfFoRNHt4E2v7pb8XoSjbBxwdhNCk9Gzmgs,1757
|
|
4
|
-
dsa_helpers/imread.py,sha256=jPr6hO4TOtGphENJQWLLtTaQ377yAwLe4F9jSAehJXQ,885
|
|
5
|
-
dsa_helpers/imwrite.py,sha256=eQI8VGn7M35_85QMPrNqGgS66iQn9YboCS50tigAE8s,1159
|
|
6
|
-
dsa_helpers/login.py,sha256=0eBhZ9sGYtweeuN3vB0HHtPl3NwYU4nlr0R2-R9wnUo,1060
|
|
7
|
-
dsa_helpers-0.3.dev0.dist-info/LICENSE,sha256=2bm9uFabQZ3Ykb_SaSU_uUbAj2-htc6WJQmS_65qD00,1073
|
|
8
|
-
dsa_helpers-0.3.dev0.dist-info/METADATA,sha256=gZRuSlrMN5yzkLq693G9yvgFXmBOWOunsgVX2ie3tB4,995
|
|
9
|
-
dsa_helpers-0.3.dev0.dist-info/WHEEL,sha256=-oYQCr74JF3a37z2nRlQays_SX2MqOANoqVjBBAP2yE,91
|
|
10
|
-
dsa_helpers-0.3.dev0.dist-info/top_level.txt,sha256=BmAEDVvDa1MJlSUFHWzx73UKec3o1K6dFUd9tHwgHGo,12
|
|
11
|
-
dsa_helpers-0.3.dev0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|