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.
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/.gitignore +3 -1
- {dsa_helpers-0.3.dev0/dsa_helpers.egg-info → dsa_helpers-0.3.dev1}/PKG-INFO +10 -6
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/__init__.py +1 -1
- dsa_helpers-0.3.dev1/dsa_helpers/dash/__init__.py +1 -0
- dsa_helpers-0.3.dev1/dsa_helpers/dash/header.py +141 -0
- dsa_helpers-0.3.dev1/dsa_helpers/dash/header_callbacks.py +98 -0
- dsa_helpers-0.3.dev1/dsa_helpers/girder_utils.py +287 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/login.py +3 -1
- dsa_helpers-0.3.dev1/dsa_helpers/mongo_utils.py +147 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1/dsa_helpers.egg-info}/PKG-INFO +10 -6
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/SOURCES.txt +7 -1
- dsa_helpers-0.3.dev1/dsa_helpers.egg-info/requires.txt +9 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/pyproject.toml +10 -6
- dsa_helpers-0.3.dev1/readthedocs.yaml +27 -0
- dsa_helpers-0.3.dev1/requirements.txt +10 -0
- dsa_helpers-0.3.dev0/dsa_helpers/girder_utils.py +0 -116
- dsa_helpers-0.3.dev0/dsa_helpers.egg-info/requires.txt +0 -5
- dsa_helpers-0.3.dev0/requirements.txt +0 -5
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/LICENSE +0 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/README.md +0 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/image_utils.py +0 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-0.3.dev0 → dsa_helpers-0.3.dev1}/setup.cfg +0 -0
|
@@ -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 @@
|
|
|
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}®ionWidth="
|
|
108
|
+
f"{width}®ionHeight={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
|
|
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.
|
|
@@ -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
|
|
@@ -4,11 +4,15 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
dependencies = [
|
|
7
|
-
'girder-client
|
|
8
|
-
'numpy
|
|
9
|
-
'setuptools
|
|
10
|
-
'opencv-python
|
|
11
|
-
'pandas
|
|
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.
|
|
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
|
|
@@ -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}®ionWidth="
|
|
108
|
-
f"{width}®ionHeight={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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|