dsa-helpers 0.3.dev1__tar.gz → 0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/.gitignore +2 -1
- dsa_helpers-0.4/PKG-INFO +50 -0
- dsa_helpers-0.4/README.md +21 -0
- dsa_helpers-0.4/app.py +14 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/__init__.py +3 -2
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/dash/header.py +31 -21
- dsa_helpers-0.4/dsa_helpers/dash/header_callbacks.py +99 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/girder_utils.py +60 -1
- dsa_helpers-0.4/dsa_helpers/ml/__init__.py +2 -0
- dsa_helpers-0.4/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +33 -0
- dsa_helpers-0.4/dsa_helpers/ml/datasets/__init__.py +3 -0
- dsa_helpers-0.4/dsa_helpers/ml/datasets/utils.py +42 -0
- dsa_helpers-0.4/dsa_helpers/ml/metrics/__init__.py +3 -0
- dsa_helpers-0.4/dsa_helpers/ml/metrics/binary_dice_coefficient_metric.py +24 -0
- dsa_helpers-0.4/dsa_helpers/ml/object_detection/__init__.py +5 -0
- dsa_helpers-0.4/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +136 -0
- dsa_helpers-0.4/dsa_helpers/ml/object_detection/tile_image.py +137 -0
- dsa_helpers-0.4/dsa_helpers/ml/object_detection/utils.py +104 -0
- dsa_helpers-0.4/dsa_helpers/ml/transforms/__init__.py +1 -0
- dsa_helpers-0.4/dsa_helpers/ml/transforms/segformer_transforms.py +23 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/mongo_utils.py +55 -42
- dsa_helpers-0.4/dsa_helpers/utils.py +28 -0
- dsa_helpers-0.4/dsa_helpers.egg-info/PKG-INFO +50 -0
- dsa_helpers-0.4/dsa_helpers.egg-info/SOURCES.txt +38 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers.egg-info/requires.txt +6 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/pyproject.toml +8 -2
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/requirements.txt +6 -1
- dsa_helpers-0.4/tests/app/Dockerfile +18 -0
- dsa_helpers-0.4/tests/app/app.py +55 -0
- dsa_helpers-0.4/tests/docker-compose.yaml +31 -0
- dsa_helpers-0.4/tests/test.ipynb +268 -0
- dsa_helpers-0.3.dev1/PKG-INFO +0 -29
- dsa_helpers-0.3.dev1/README.md +0 -6
- dsa_helpers-0.3.dev1/dsa_helpers/dash/header_callbacks.py +0 -98
- dsa_helpers-0.3.dev1/dsa_helpers.egg-info/PKG-INFO +0 -29
- dsa_helpers-0.3.dev1/dsa_helpers.egg-info/SOURCES.txt +0 -22
- dsa_helpers-0.3.dev1/readthedocs.yaml +0 -27
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/LICENSE +0 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/dash/__init__.py +0 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/image_utils.py +0 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers/login.py +0 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-0.3.dev1 → dsa_helpers-0.4}/setup.cfg +0 -0
dsa_helpers-0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: dsa_helpers
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: Utility functions for working with the DSA girder client.
|
|
5
|
+
Author-email: "Juan C. Vizcarra" <jvizcar@emory.edu>
|
|
6
|
+
Maintainer-email: "Juan C. Vizcarra" <jvizcar@emory.edu>
|
|
7
|
+
Project-URL: Repository, https://github.com/Gutman-Lab/dsa-python-utils
|
|
8
|
+
Keywords: dsa,girder,girder_client
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
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
|
|
19
|
+
Requires-Dist: pymongo
|
|
20
|
+
Requires-Dist: pillow
|
|
21
|
+
Requires-Dist: dash
|
|
22
|
+
Requires-Dist: dash_bootstrap_components
|
|
23
|
+
Requires-Dist: shapely
|
|
24
|
+
Requires-Dist: geopandas
|
|
25
|
+
Requires-Dist: torch
|
|
26
|
+
Requires-Dist: datasets
|
|
27
|
+
Requires-Dist: transformers
|
|
28
|
+
Requires-Dist: torchvision
|
|
29
|
+
|
|
30
|
+
# Digital Slide Archive (DSA) Helpers
|
|
31
|
+
Digital Slide Archive Python utility library.
|
|
32
|
+
|
|
33
|
+
This library is available for installation through [Python Package Index (PyPI)](https://pypi.org/).
|
|
34
|
+
|
|
35
|
+
This library was tested using Python version 3.11.8 and uses the dependencies described in requirements.txt.
|
|
36
|
+
|
|
37
|
+
This Python PyPI package is found [here](https://pypi.org/project/dsa-helpers/).
|
|
38
|
+
|
|
39
|
+
## Instructions for Development
|
|
40
|
+
1. Install requirements for building and distributing package:
|
|
41
|
+
```
|
|
42
|
+
$ python3 -m pip install --upgrade build # for building package
|
|
43
|
+
$ python3 -m pip install --upgrade twine # for uploading to PyPI
|
|
44
|
+
```
|
|
45
|
+
2. When ready to build:
|
|
46
|
+
- Modify "pyproject.toml" file, change the "version" key to a different value than a version already pushed.
|
|
47
|
+
- Run ```$ python3 -m build```. This will create a dist directory and put your new wheel and tar distribution files there.
|
|
48
|
+
3. To install locally for development: ```$ pip3 install dist/dsa_helpers-*.whl```, choosing the wheel version you want to test.
|
|
49
|
+
4. Upload to PyPI using twine: ```python3 -m twine upload --repository pypi dist/*```
|
|
50
|
+
* You can specify specific files to upload, the line above pushes everything in dist directory.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Digital Slide Archive (DSA) Helpers
|
|
2
|
+
Digital Slide Archive Python utility library.
|
|
3
|
+
|
|
4
|
+
This library is available for installation through [Python Package Index (PyPI)](https://pypi.org/).
|
|
5
|
+
|
|
6
|
+
This library was tested using Python version 3.11.8 and uses the dependencies described in requirements.txt.
|
|
7
|
+
|
|
8
|
+
This Python PyPI package is found [here](https://pypi.org/project/dsa-helpers/).
|
|
9
|
+
|
|
10
|
+
## Instructions for Development
|
|
11
|
+
1. Install requirements for building and distributing package:
|
|
12
|
+
```
|
|
13
|
+
$ python3 -m pip install --upgrade build # for building package
|
|
14
|
+
$ python3 -m pip install --upgrade twine # for uploading to PyPI
|
|
15
|
+
```
|
|
16
|
+
2. When ready to build:
|
|
17
|
+
- Modify "pyproject.toml" file, change the "version" key to a different value than a version already pushed.
|
|
18
|
+
- Run ```$ python3 -m build```. This will create a dist directory and put your new wheel and tar distribution files there.
|
|
19
|
+
3. To install locally for development: ```$ pip3 install dist/dsa_helpers-*.whl```, choosing the wheel version you want to test.
|
|
20
|
+
4. Upload to PyPI using twine: ```python3 -m twine upload --repository pypi dist/*```
|
|
21
|
+
* You can specify specific files to upload, the line above pushes everything in dist directory.
|
dsa_helpers-0.4/app.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# A test application.
|
|
2
|
+
from dash import Dash, html
|
|
3
|
+
import dash_bootstrap_components as dbc
|
|
4
|
+
from dsa_helpers.dash.header import get_header
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
os.environ["DSA_API_URL"] = "https://megabrain.neurology.emory.edu/api/v1"
|
|
8
|
+
|
|
9
|
+
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
|
|
10
|
+
|
|
11
|
+
app.layout = html.Div([get_header("Test")])
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
app.run_server(debug=True)
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Shadow imports.
|
|
2
|
-
from .login import login
|
|
3
2
|
from .imread import imread
|
|
4
3
|
from .imwrite import imwrite
|
|
5
4
|
|
|
6
5
|
# Modules that should be available.
|
|
7
|
-
from . import girder_utils, image_utils, dash
|
|
6
|
+
# from . import girder_utils, image_utils, dash
|
|
7
|
+
|
|
8
|
+
__all__ = ["girder_utils", "image_utils", "dash", "ml"]
|
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
from dash import html, dcc
|
|
2
2
|
import dash_bootstrap_components as dbc
|
|
3
|
-
from . import
|
|
3
|
+
from .header_callbacks import get_callbacks
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def get_header(
|
|
6
|
+
def get_header(
|
|
7
|
+
dsa_api_url: str,
|
|
8
|
+
title: str = "App",
|
|
9
|
+
store_id: str = "user-store",
|
|
10
|
+
config: dict | None = None,
|
|
11
|
+
) -> html.Div:
|
|
7
12
|
"""Get the Dash html component that contains the header of the application
|
|
8
13
|
with login capabilities.
|
|
9
14
|
|
|
10
15
|
Args:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
dsa_api_url (str): The URL of the DSA API.
|
|
17
|
+
title (str, optional): The title of the application. Defaults to "App".
|
|
18
|
+
config (dict, optional): The configuration of the component. Defaults to None.
|
|
19
|
+
If None the default configuration is:
|
|
20
|
+
{
|
|
21
|
+
"backgroundColor": "#012169",
|
|
22
|
+
"titleColor": "#d9d9d6",
|
|
23
|
+
"fontColor": "#f2a900",
|
|
24
|
+
}
|
|
17
25
|
|
|
18
26
|
Returns:
|
|
19
27
|
html.Div: The header component.
|
|
20
28
|
|
|
21
29
|
"""
|
|
30
|
+
if config is None:
|
|
31
|
+
config = {}
|
|
32
|
+
|
|
33
|
+
get_callbacks(dsa_api_url, store_id)
|
|
34
|
+
|
|
22
35
|
login_modal = dbc.Modal(
|
|
23
36
|
[
|
|
24
37
|
dbc.ModalHeader("Log in"),
|
|
@@ -28,7 +41,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
28
41
|
"Login or email", style={"margin": 5, "fontWeight": "bold"}
|
|
29
42
|
),
|
|
30
43
|
dbc.Input(
|
|
31
|
-
id="login",
|
|
44
|
+
id=f"{store_id}=login",
|
|
32
45
|
type="text",
|
|
33
46
|
placeholder="Enter login",
|
|
34
47
|
style={"margin": 5},
|
|
@@ -38,7 +51,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
38
51
|
style={"margin": 5, "marginTop": 15, "fontWeight": "bold"},
|
|
39
52
|
),
|
|
40
53
|
dbc.Input(
|
|
41
|
-
id="password",
|
|
54
|
+
id=f"{store_id}=password",
|
|
42
55
|
type="password",
|
|
43
56
|
placeholder="Enter password",
|
|
44
57
|
style={"margin": 5},
|
|
@@ -46,7 +59,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
46
59
|
html.Div(
|
|
47
60
|
"Login failed.",
|
|
48
61
|
hidden=True,
|
|
49
|
-
id="login-failed",
|
|
62
|
+
id=f"{store_id}=login-failed",
|
|
50
63
|
style={"color": "red", "fontWeight": "bold", "margin": 10},
|
|
51
64
|
),
|
|
52
65
|
],
|
|
@@ -57,7 +70,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
57
70
|
dbc.Col(
|
|
58
71
|
dbc.Button(
|
|
59
72
|
"Close",
|
|
60
|
-
id="close-login-modal",
|
|
73
|
+
id=f"{store_id}=close-login-modal",
|
|
61
74
|
className="me-1",
|
|
62
75
|
color="light",
|
|
63
76
|
)
|
|
@@ -65,7 +78,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
65
78
|
dbc.Col(
|
|
66
79
|
dbc.Button(
|
|
67
80
|
"Login",
|
|
68
|
-
id="log-in-btn",
|
|
81
|
+
id=f"{store_id}=log-in-btn",
|
|
69
82
|
className="me-1",
|
|
70
83
|
color="primary",
|
|
71
84
|
)
|
|
@@ -75,7 +88,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
75
88
|
),
|
|
76
89
|
],
|
|
77
90
|
is_open=False,
|
|
78
|
-
id="login-modal",
|
|
91
|
+
id=f"{store_id}=login-modal",
|
|
79
92
|
)
|
|
80
93
|
|
|
81
94
|
logout_modal = dbc.Modal(
|
|
@@ -84,7 +97,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
84
97
|
[
|
|
85
98
|
dbc.Button(
|
|
86
99
|
"Log out",
|
|
87
|
-
id="logout-btn",
|
|
100
|
+
id=f"{store_id}=logout-btn",
|
|
88
101
|
color="danger",
|
|
89
102
|
className="me-1",
|
|
90
103
|
),
|
|
@@ -92,12 +105,9 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
92
105
|
)
|
|
93
106
|
],
|
|
94
107
|
is_open=False,
|
|
95
|
-
id="logout-modal",
|
|
108
|
+
id=f"{store_id}=logout-modal",
|
|
96
109
|
)
|
|
97
110
|
|
|
98
|
-
if config is None:
|
|
99
|
-
config = {}
|
|
100
|
-
|
|
101
111
|
return html.Div(
|
|
102
112
|
[
|
|
103
113
|
dbc.Row(
|
|
@@ -116,7 +126,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
116
126
|
dbc.Col(
|
|
117
127
|
dbc.Button(
|
|
118
128
|
"Log in",
|
|
119
|
-
id="login-btn",
|
|
129
|
+
id=f"{store_id}=login-btn",
|
|
120
130
|
color=config.get("backgroundColor", "#012169"),
|
|
121
131
|
style={
|
|
122
132
|
"color": config.get("fontColor", "#f2a900"),
|
|
@@ -132,7 +142,7 @@ def get_header(title: str = "App", config: dict | None = None) -> html.Div:
|
|
|
132
142
|
),
|
|
133
143
|
login_modal,
|
|
134
144
|
logout_modal,
|
|
135
|
-
dcc.Store(id=
|
|
145
|
+
dcc.Store(id=store_id, storage_type="local", data={}),
|
|
136
146
|
],
|
|
137
147
|
style={
|
|
138
148
|
"backgroundColor": config.get("backgroundColor", "#012169"),
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Callbacks for the header component.
|
|
2
|
+
from dash import callback, Output, Input, State, no_update
|
|
3
|
+
from girder_client import GirderClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_callbacks(dsa_api_url: str, store_id: str):
|
|
7
|
+
"""Setup the callbacks to the DSA API url of interest and the proper store id."""
|
|
8
|
+
|
|
9
|
+
@callback(Output(f"{store_id}=login-btn", "children"), Input(store_id, "data"))
|
|
10
|
+
def check_user_store(data):
|
|
11
|
+
# Check if the user store has user info or if no one is logged in.
|
|
12
|
+
return data["user"] if len(data) else "Log in"
|
|
13
|
+
|
|
14
|
+
@callback(
|
|
15
|
+
[
|
|
16
|
+
Output(f"{store_id}=login-modal", "is_open", allow_duplicate=True),
|
|
17
|
+
Output(f"{store_id}=logout-modal", "is_open", allow_duplicate=True),
|
|
18
|
+
],
|
|
19
|
+
[
|
|
20
|
+
Input(f"{store_id}=login-btn", "n_clicks"),
|
|
21
|
+
State(f"{store_id}=login-btn", "children"),
|
|
22
|
+
],
|
|
23
|
+
prevent_initial_call=True,
|
|
24
|
+
)
|
|
25
|
+
def open_login_modal(n_clicks, children):
|
|
26
|
+
# Open login / logout modal.
|
|
27
|
+
if n_clicks:
|
|
28
|
+
if children == "Log in":
|
|
29
|
+
return True, False
|
|
30
|
+
else:
|
|
31
|
+
return False, True
|
|
32
|
+
|
|
33
|
+
return False, False
|
|
34
|
+
|
|
35
|
+
@callback(
|
|
36
|
+
[
|
|
37
|
+
Output(store_id, "data"),
|
|
38
|
+
Output(f"{store_id}=login-failed", "hidden", allow_duplicate=True),
|
|
39
|
+
Output(f"{store_id}=login-modal", "is_open", allow_duplicate=True),
|
|
40
|
+
Output(f"{store_id}=login", "value", allow_duplicate=True),
|
|
41
|
+
Output(f"{store_id}=password", "value", allow_duplicate=True),
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
Input(f"{store_id}=log-in-btn", "n_clicks"),
|
|
45
|
+
State(f"{store_id}=login", "value"),
|
|
46
|
+
State(f"{store_id}=password", "value"),
|
|
47
|
+
],
|
|
48
|
+
prevent_initial_call=True,
|
|
49
|
+
)
|
|
50
|
+
def login(n_clicks, login, password):
|
|
51
|
+
# Try to login.
|
|
52
|
+
gc = GirderClient(apiUrl=dsa_api_url)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
_ = gc.authenticate(username=login, password=password)
|
|
56
|
+
|
|
57
|
+
response = gc.get("token/session")
|
|
58
|
+
|
|
59
|
+
user = gc.get("user/me")["login"]
|
|
60
|
+
|
|
61
|
+
return {"user": user, "token": response["token"]}, True, False, "", ""
|
|
62
|
+
except:
|
|
63
|
+
return (
|
|
64
|
+
{},
|
|
65
|
+
False,
|
|
66
|
+
True,
|
|
67
|
+
no_update,
|
|
68
|
+
no_update,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
@callback(
|
|
72
|
+
[
|
|
73
|
+
Output(f"{store_id}=login-modal", "is_open", allow_duplicate=True),
|
|
74
|
+
Output(f"{store_id}=login", "value", allow_duplicate=True),
|
|
75
|
+
Output(f"{store_id}=password", "value", allow_duplicate=True),
|
|
76
|
+
Output(f"{store_id}=login-failed", "hidden", allow_duplicate=True),
|
|
77
|
+
],
|
|
78
|
+
Input(f"{store_id}=close-login-modal", "n_clicks"),
|
|
79
|
+
prevent_initial_call=True,
|
|
80
|
+
)
|
|
81
|
+
def close_login_modal(n_clicks):
|
|
82
|
+
if n_clicks:
|
|
83
|
+
return False, "", "", True
|
|
84
|
+
|
|
85
|
+
return False, "", "", True
|
|
86
|
+
|
|
87
|
+
@callback(
|
|
88
|
+
[
|
|
89
|
+
Output(store_id, "data", allow_duplicate=True),
|
|
90
|
+
Output(f"{store_id}=logout-modal", "is_open", allow_duplicate=True),
|
|
91
|
+
],
|
|
92
|
+
Input(f"{store_id}=logout-btn", "n_clicks"),
|
|
93
|
+
prevent_initial_call=True,
|
|
94
|
+
)
|
|
95
|
+
def logout(n_clicks):
|
|
96
|
+
if n_clicks:
|
|
97
|
+
return {}, False
|
|
98
|
+
|
|
99
|
+
return no_update, False
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Functions using the girder client.
|
|
2
|
-
from girder_client import GirderClient
|
|
2
|
+
from girder_client import GirderClient, HttpError
|
|
3
3
|
import pickle
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pandas as pd
|
|
@@ -285,3 +285,62 @@ def get_rectangle_element_coords(element):
|
|
|
285
285
|
corner_coords = np.array(corner_coords, dtype=np.int32)
|
|
286
286
|
|
|
287
287
|
return corner_coords
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def login(
|
|
291
|
+
api_url: str,
|
|
292
|
+
login_or_email: str | None = None,
|
|
293
|
+
password: str | None = None,
|
|
294
|
+
api_key: str | None = None,
|
|
295
|
+
) -> GirderClient:
|
|
296
|
+
"""Authenticate a girder client with the given credentials or interactively
|
|
297
|
+
if none is given.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
api_url (str): The DSA girder API url.
|
|
301
|
+
login_or_email (str | None): The login or email. Defaults to None.
|
|
302
|
+
password (str | None): Password for login / email. Defaults to None.
|
|
303
|
+
api_key (str | None): The api key to authenticate with. Defaults to None.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
girder_client.GirderClient: The authenticated girder client.
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
gc = GirderClient(apiUrl=api_url)
|
|
310
|
+
|
|
311
|
+
if api_key is None:
|
|
312
|
+
if login_or_email is None:
|
|
313
|
+
_ = gc.authenticate(interactive=True)
|
|
314
|
+
elif password is None:
|
|
315
|
+
_ = gc.authenticate(username=login_or_email, interactive=True)
|
|
316
|
+
else:
|
|
317
|
+
_ = gc.authenticate(username=login_or_email, password=password)
|
|
318
|
+
else:
|
|
319
|
+
_ = gc.authenticate(apiKey=api_key)
|
|
320
|
+
|
|
321
|
+
return gc
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def get_items(gc: GirderClient, parend_id: str) -> list[dict]:
|
|
325
|
+
"""Get the items in a parent location recursively.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
gc (girder_client.GirderClient): The authenticated girder client.
|
|
329
|
+
parend_id (str): The parent id to start the search (folder / collection).
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
list[dict]: The list of items.
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
params = {"type": "folder", "limit": 0, "offset": 0, "sort": "_id", "sortdir": 1}
|
|
336
|
+
|
|
337
|
+
request_url = f"resource/{parend_id}/items"
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
items = gc.get(request_url, parameters=params)
|
|
341
|
+
except HttpError:
|
|
342
|
+
params["type"] = "collection"
|
|
343
|
+
|
|
344
|
+
items = gc.get(request_url, parameters=params)
|
|
345
|
+
|
|
346
|
+
return items
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from torch.utils.data import Dataset
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from PIL import Image
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SegFormerSegmentationDataset(Dataset):
|
|
8
|
+
"""PyTorch dataset class for semantic segmentation using HuggingFaces SegFormer
|
|
9
|
+
model.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, df: pd.DataFrame):
|
|
14
|
+
"""Initiate an instance of the segmentation dataset.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
df (pandas.DataFrame): A pandas DataFrame with columns "fp" and "mask_fp".
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
self.image_files = df["fp"].tolist()
|
|
21
|
+
self.mask_files = df["mask_fp"].tolist()
|
|
22
|
+
self.df = df # store potential metadata
|
|
23
|
+
|
|
24
|
+
def __len__(self):
|
|
25
|
+
return len(self.image_files)
|
|
26
|
+
|
|
27
|
+
def __getitem__(self, idx):
|
|
28
|
+
# Get the filepath to the image and its mask.
|
|
29
|
+
image_path = self.image_files[idx]
|
|
30
|
+
mask_path = self.mask_files[idx]
|
|
31
|
+
|
|
32
|
+
# Return image & mask as PIL images in a dictionary.
|
|
33
|
+
return {"pixel_values": Image.open(image_path), "label": Image.open(mask_path)}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Utility functions, usualy for creating Datasets in specific formats.
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from .SegFormerSegmentationDataset import SegFormerSegmentationDataset
|
|
4
|
+
from datasets import Dataset, Features, Image
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def dataset_generator(dataset):
|
|
8
|
+
"""Yield a dataset."""
|
|
9
|
+
for item in dataset:
|
|
10
|
+
yield item
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_segformer_segmentation_dataset(
|
|
14
|
+
df: pd.DataFrame | str, low_memory: bool = True, transforms=None
|
|
15
|
+
):
|
|
16
|
+
"""Create a SegFormer segmentation dataset from a DataFrame.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
df (pd.DataFrame | str): A pandas DataFrame with columns "fp" and "mask_fp" or a
|
|
20
|
+
path to a CSV file.
|
|
21
|
+
low_memory (bool): Whether to read the CSV file in low memory mode.
|
|
22
|
+
transforms: A function that takes in a batch of samples (dictionary with
|
|
23
|
+
pixel_values and label keys) and returns a transformed batch.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A Dataset object to be used for HuggingFaces SegFormer model training.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
if isinstance(df, str):
|
|
30
|
+
df = pd.read_csv(df, low_memory=low_memory)
|
|
31
|
+
|
|
32
|
+
dataset = SegFormerSegmentationDataset(df)
|
|
33
|
+
|
|
34
|
+
dataset = Dataset.from_generator(
|
|
35
|
+
generator=lambda: dataset_generator(dataset),
|
|
36
|
+
features=Features({"pixel_values": Image(), "label": Image()}), # Example shape
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if transforms is not None:
|
|
40
|
+
dataset.set_transform(transforms)
|
|
41
|
+
|
|
42
|
+
return dataset
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch import nn
|
|
3
|
+
from ...utils import binary_dice_coefficient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def binary_dice_coefficient_metric(eval_pred):
|
|
7
|
+
with torch.no_grad():
|
|
8
|
+
logits, labels = eval_pred # assumed both are numpy
|
|
9
|
+
|
|
10
|
+
logits_tensor = torch.from_numpy(logits).cpu()
|
|
11
|
+
labels_tensor = torch.from_numpy(labels).cpu()
|
|
12
|
+
|
|
13
|
+
# Scale the logits to the size of the label.
|
|
14
|
+
logits_tensor = nn.functional.interpolate(
|
|
15
|
+
logits_tensor,
|
|
16
|
+
size=labels.shape[-2:],
|
|
17
|
+
mode="bilinear",
|
|
18
|
+
align_corners=False,
|
|
19
|
+
).argmax(dim=1)
|
|
20
|
+
|
|
21
|
+
pred_labels = logits_tensor.detach().numpy()
|
|
22
|
+
labels = labels_tensor.numpy()
|
|
23
|
+
|
|
24
|
+
return {"dice_coefficient": binary_dice_coefficient(pred_labels, labels)}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from girder_client import GirderClient
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ...girder_utils import get_region, get_item_large_image_metadata
|
|
6
|
+
from ... import imwrite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_pearce_roi_images(
|
|
10
|
+
gc: GirderClient,
|
|
11
|
+
item_id: str,
|
|
12
|
+
class_map: dict[str, int],
|
|
13
|
+
save_dir: str = ".",
|
|
14
|
+
mag: float | None = None,
|
|
15
|
+
) -> pd.DataFrame:
|
|
16
|
+
"""Download the smallestl bounding box around an ROI annotations and its
|
|
17
|
+
accompanying object detection annotations. The format used to determine ROIs
|
|
18
|
+
and boxes is based on the convention used by Dr. Thomas Pearce in his
|
|
19
|
+
OpenSeadragon applications. Labels are saved as text files in the YOLO format.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
gc (girder_client.GirderClient): The Girder client to use.
|
|
23
|
+
item_id (str): The item ID of the WSI.
|
|
24
|
+
class_map (dict[str, int]): A dictionary mapping class names to integers.
|
|
25
|
+
save_dir (str, optional): The directory to save the images and labels.
|
|
26
|
+
Defaults to ".".
|
|
27
|
+
mag (float | None, optional): The magnification to download the ROI at.
|
|
28
|
+
If None, the scan magnification is used. Defaults to None.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
pandas.DataFrame: A DataFrame containing metadata for each ROI
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
# Create directories to save images and labels.
|
|
35
|
+
img_dir = Path(save_dir) / "images"
|
|
36
|
+
img_dir.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
label_dir = Path(save_dir) / "labels"
|
|
39
|
+
label_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
# Get all the annotation documents with name starting with "ROI".
|
|
42
|
+
ann_docs_ids = []
|
|
43
|
+
|
|
44
|
+
for ann_doc in gc.get(
|
|
45
|
+
f"annotation?itemId={item_id}&limit=0&offset=0&sort=lowerName&sortdir=1"
|
|
46
|
+
):
|
|
47
|
+
if ann_doc.get("annotation", {}).get("name", "").startswith("ROI"):
|
|
48
|
+
ann_docs_ids.append(ann_doc["_id"])
|
|
49
|
+
|
|
50
|
+
if not len(ann_docs_ids):
|
|
51
|
+
# No annotation documents, return None.
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# Track metadata for each ROI.
|
|
55
|
+
roi_metadata = []
|
|
56
|
+
|
|
57
|
+
# Loop through each document.
|
|
58
|
+
for ann_doc_id in ann_docs_ids:
|
|
59
|
+
# Get the full annotation document.
|
|
60
|
+
ann_doc = gc.get(f"annotation/{ann_doc_id}")
|
|
61
|
+
|
|
62
|
+
# Separate the ROI element and the other elements.
|
|
63
|
+
roi_el = None
|
|
64
|
+
other_els = []
|
|
65
|
+
|
|
66
|
+
for element in ann_doc.get("annotation", {}).get("elements", []):
|
|
67
|
+
if element.get("user", {}).get("role") == "ROI":
|
|
68
|
+
roi_el = element
|
|
69
|
+
else:
|
|
70
|
+
other_els.append(element)
|
|
71
|
+
|
|
72
|
+
if roi_el is None:
|
|
73
|
+
raise Exception("No ROI element found in annotation document.")
|
|
74
|
+
|
|
75
|
+
# Download the ROI image.
|
|
76
|
+
xc, yc = roi_el["center"][:2]
|
|
77
|
+
roi_w, roi_h = int(roi_el["width"]), int(roi_el["height"])
|
|
78
|
+
|
|
79
|
+
# Get the top left coordinate of the ROI.
|
|
80
|
+
roi_left = int(xc - roi_w / 2)
|
|
81
|
+
roi_top = int(yc - roi_h / 2)
|
|
82
|
+
|
|
83
|
+
# Get scale factor to go from scan magnification to mag.
|
|
84
|
+
scan_mag = get_item_large_image_metadata(gc, item_id)["magnification"]
|
|
85
|
+
|
|
86
|
+
if mag is None:
|
|
87
|
+
mag = scan_mag
|
|
88
|
+
|
|
89
|
+
roi_img = get_region(
|
|
90
|
+
gc, item_id, left=roi_left, top=roi_top, width=roi_w, height=roi_h, mag=mag
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
fn = f"itemId-{item_id}-x{roi_left}y{roi_top}x{roi_left+roi_w}y{roi_top+roi_h}"
|
|
94
|
+
|
|
95
|
+
img_fp = img_dir / f"{fn}.png"
|
|
96
|
+
imwrite(img_fp, roi_img)
|
|
97
|
+
|
|
98
|
+
# Convert the boxes to YOLO format.
|
|
99
|
+
labels = ""
|
|
100
|
+
|
|
101
|
+
for element in other_els:
|
|
102
|
+
el_class = element["user"]["class"]
|
|
103
|
+
|
|
104
|
+
if el_class not in class_map:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
xc, yc = element["center"][:2]
|
|
108
|
+
xc, yc = int(xc), int(yc)
|
|
109
|
+
|
|
110
|
+
# Subtract the left and top of the ROI to get the correct coordinates.
|
|
111
|
+
xc = xc - roi_left
|
|
112
|
+
yc = yc - roi_top
|
|
113
|
+
|
|
114
|
+
# Normalize by the ROI width and height.
|
|
115
|
+
xc /= roi_w
|
|
116
|
+
yc /= roi_h
|
|
117
|
+
|
|
118
|
+
w = element["width"] / roi_w
|
|
119
|
+
h = element["height"] / roi_h
|
|
120
|
+
|
|
121
|
+
labels += f"{class_map[el_class]} {xc:.4f} {yc:.4f} {w:.4f} {h:.4f}\n"
|
|
122
|
+
|
|
123
|
+
# Save the label to a text file.
|
|
124
|
+
label_fp = label_dir / f"{fn}.txt"
|
|
125
|
+
|
|
126
|
+
with open(label_fp, "w") as fh:
|
|
127
|
+
fh.write(labels.strip())
|
|
128
|
+
|
|
129
|
+
# Append all the metadata to the list.
|
|
130
|
+
roi_metadata.append(
|
|
131
|
+
[item_id, img_fp, roi_left, roi_top, roi_w, roi_h, mag, scan_mag]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return pd.DataFrame(
|
|
135
|
+
roi_metadata, columns=["item_id", "fp", "x", "y", "w", "h", "mag", "scan_mag"]
|
|
136
|
+
)
|