dsa-helpers 0.8.dev1__tar.gz → 0.9__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.9/.readthedocs.yaml +16 -0
- {dsa_helpers-0.8.dev1/dsa_helpers.egg-info → dsa_helpers-0.9}/PKG-INFO +5 -3
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/README.md +3 -0
- dsa_helpers-0.9/docs/requirements.txt +16 -0
- dsa_helpers-0.9/docs/source/conf.py +42 -0
- dsa_helpers-0.9/docs/source/index.rst +18 -0
- dsa_helpers-0.9/dsa_helpers/__init__.py +23 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/dash/header.py +14 -10
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/girder_utils.py +109 -20
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/__init__.py +0 -1
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/datasets/__init__.py +1 -1
- dsa_helpers-0.9/dsa_helpers/ml/metrics.py +117 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/object_detection/__init__.py +1 -1
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +1 -1
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +4 -2
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/tiling/__init__.py +1 -1
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/tiling/semantic_seg_tile_wsi_with_dsa_annotations.py +6 -5
- dsa_helpers-0.9/dsa_helpers/tile_utils.py +48 -0
- dsa_helpers-0.9/dsa_helpers/tiling.py +340 -0
- dsa_helpers-0.9/dsa_helpers/utils.py +75 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9/dsa_helpers.egg-info}/PKG-INFO +5 -3
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers.egg-info/SOURCES.txt +7 -4
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers.egg-info/requires.txt +1 -2
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/pyproject.toml +2 -3
- dsa_helpers-0.9/tests/test.ipynb +141 -0
- dsa_helpers-0.8.dev1/dsa_helpers/__init__.py +0 -16
- dsa_helpers-0.8.dev1/dsa_helpers/image_utils.py +0 -74
- dsa_helpers-0.8.dev1/dsa_helpers/ml/metrics/__init__.py +0 -3
- dsa_helpers-0.8.dev1/dsa_helpers/ml/metrics/binary_dice_coefficient_metric.py +0 -24
- dsa_helpers-0.8.dev1/dsa_helpers/utils.py +0 -28
- dsa_helpers-0.8.dev1/requirements.txt +0 -23
- dsa_helpers-0.8.dev1/tests/test.ipynb +0 -209
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/.gitignore +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/LICENSE +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/app.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/dash/__init__.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/dash/header_callbacks.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/datasets/utils.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/object_detection/utils.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/transforms/__init__.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/mongo_utils.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/setup.cfg +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/tests/app/Dockerfile +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/tests/app/app.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/tests/docker-compose.yaml +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
build:
|
|
4
|
+
os: "ubuntu-24.04"
|
|
5
|
+
tools:
|
|
6
|
+
python: "3.12"
|
|
7
|
+
jobs:
|
|
8
|
+
pre_build:
|
|
9
|
+
- sphinx-apidoc -o docs/source dsa_helpers/
|
|
10
|
+
|
|
11
|
+
python:
|
|
12
|
+
install:
|
|
13
|
+
- requirements: docs/requirements.txt
|
|
14
|
+
|
|
15
|
+
sphinx:
|
|
16
|
+
configuration: docs/source/conf.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dsa_helpers
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9
|
|
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>
|
|
@@ -19,11 +19,10 @@ Requires-Dist: pandas
|
|
|
19
19
|
Requires-Dist: pymongo
|
|
20
20
|
Requires-Dist: pillow
|
|
21
21
|
Requires-Dist: dash
|
|
22
|
-
Requires-Dist:
|
|
22
|
+
Requires-Dist: dash-bootstrap-components
|
|
23
23
|
Requires-Dist: shapely
|
|
24
24
|
Requires-Dist: geopandas
|
|
25
25
|
Requires-Dist: transformers[torch]
|
|
26
|
-
Requires-Dist: torch
|
|
27
26
|
Requires-Dist: datasets
|
|
28
27
|
Requires-Dist: torchvision
|
|
29
28
|
Requires-Dist: large-image
|
|
@@ -37,6 +36,9 @@ This library was tested using Python version 3.11.8 and uses the dependencies de
|
|
|
37
36
|
|
|
38
37
|
This Python PyPI package is found [here](https://pypi.org/project/dsa-helpers/).
|
|
39
38
|
|
|
39
|
+
## Extra dependencies
|
|
40
|
+
If you are getting a large image error, then install large image using the instructions provided in their [GitHub page](https://github.com/girder/large_image#:~:text=pip%20install%20large%2Dimage%5Ball%5D%20%2D%2Dfind%2Dlinks%20https%3A//girder.github.io/large_image_wheels).
|
|
41
|
+
|
|
40
42
|
## Instructions for Development
|
|
41
43
|
1. Install requirements for building and distributing package:
|
|
42
44
|
```
|
|
@@ -7,6 +7,9 @@ This library was tested using Python version 3.11.8 and uses the dependencies de
|
|
|
7
7
|
|
|
8
8
|
This Python PyPI package is found [here](https://pypi.org/project/dsa-helpers/).
|
|
9
9
|
|
|
10
|
+
## Extra dependencies
|
|
11
|
+
If you are getting a large image error, then install large image using the instructions provided in their [GitHub page](https://github.com/girder/large_image#:~:text=pip%20install%20large%2Dimage%5Ball%5D%20%2D%2Dfind%2Dlinks%20https%3A//girder.github.io/large_image_wheels).
|
|
12
|
+
|
|
10
13
|
## Instructions for Development
|
|
11
14
|
1. Install requirements for building and distributing package:
|
|
12
15
|
```
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Configuration file for the Sphinx documentation builder.
|
|
2
|
+
#
|
|
3
|
+
# For the full list of built-in configuration values, see the documentation:
|
|
4
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
5
|
+
|
|
6
|
+
# -- Path setup --------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
# If extensions (or modules to document with autodoc) are in another directory,
|
|
9
|
+
# add these directories to sys.path here. If the directory is relative to the
|
|
10
|
+
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
11
|
+
#
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.abspath("../.."))
|
|
16
|
+
|
|
17
|
+
# -- Project information -----------------------------------------------------
|
|
18
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
|
19
|
+
|
|
20
|
+
project = "DSA Helpers"
|
|
21
|
+
copyright = "2024, Juan Carlos Vizcarra"
|
|
22
|
+
author = "Juan Carlos Vizcarra"
|
|
23
|
+
release = "00.00.01"
|
|
24
|
+
|
|
25
|
+
# -- General configuration ---------------------------------------------------
|
|
26
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
27
|
+
|
|
28
|
+
extensions = [
|
|
29
|
+
"sphinx.ext.autodoc",
|
|
30
|
+
"sphinx.ext.viewcode",
|
|
31
|
+
"sphinx.ext.napoleon",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
templates_path = ["_templates"]
|
|
35
|
+
exclude_patterns = []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# -- Options for HTML output -------------------------------------------------
|
|
39
|
+
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
|
40
|
+
|
|
41
|
+
html_theme = "sphinx_rtd_theme"
|
|
42
|
+
html_static_path = ["_static"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.. DSA Helpers documentation master file, created by
|
|
2
|
+
sphinx-quickstart on Fri Dec 6 12:07:31 2024.
|
|
3
|
+
You can adapt this file completely to your liking, but it should at least
|
|
4
|
+
contain the root `toctree` directive.
|
|
5
|
+
|
|
6
|
+
DSA Helpers documentation
|
|
7
|
+
=========================
|
|
8
|
+
|
|
9
|
+
Add your content using ``reStructuredText`` syntax. See the
|
|
10
|
+
`reStructuredText <https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html>`_
|
|
11
|
+
documentation for details.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
.. toctree::
|
|
15
|
+
:maxdepth: 2
|
|
16
|
+
:caption: Contents:
|
|
17
|
+
|
|
18
|
+
modules
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Shadow imports.
|
|
2
|
+
from .imread import imread
|
|
3
|
+
from .imwrite import imwrite
|
|
4
|
+
|
|
5
|
+
# Make modules available.
|
|
6
|
+
from . import girder_utils
|
|
7
|
+
from . import dash
|
|
8
|
+
from . import ml
|
|
9
|
+
from . import mongo_utils
|
|
10
|
+
from . import tiling
|
|
11
|
+
from . import utils
|
|
12
|
+
from . import tile_utils
|
|
13
|
+
|
|
14
|
+
# Modules that should be available.
|
|
15
|
+
__all__ = [
|
|
16
|
+
"girder_utils",
|
|
17
|
+
"dash",
|
|
18
|
+
"ml",
|
|
19
|
+
"mongo_utils",
|
|
20
|
+
"tiling",
|
|
21
|
+
"utils",
|
|
22
|
+
"tile_utils",
|
|
23
|
+
]
|
|
@@ -15,13 +15,8 @@ def get_header(
|
|
|
15
15
|
Args:
|
|
16
16
|
dsa_api_url (str): The URL of the DSA API.
|
|
17
17
|
title (str, optional): The title of the application. Defaults to "App".
|
|
18
|
-
config (dict, optional): The configuration of the component.
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
"backgroundColor": "#012169",
|
|
22
|
-
"titleColor": "#d9d9d6",
|
|
23
|
-
"fontColor": "#f2a900",
|
|
24
|
-
}
|
|
18
|
+
config (dict, optional): The configuration of the component.
|
|
19
|
+
Defaults to None which uses default settings.
|
|
25
20
|
|
|
26
21
|
Returns:
|
|
27
22
|
html.Div: The header component.
|
|
@@ -38,7 +33,8 @@ def get_header(
|
|
|
38
33
|
dbc.ModalBody(
|
|
39
34
|
[
|
|
40
35
|
html.Div(
|
|
41
|
-
"Login or email",
|
|
36
|
+
"Login or email",
|
|
37
|
+
style={"margin": 5, "fontWeight": "bold"},
|
|
42
38
|
),
|
|
43
39
|
dbc.Input(
|
|
44
40
|
id=f"{store_id}=login",
|
|
@@ -48,7 +44,11 @@ def get_header(
|
|
|
48
44
|
),
|
|
49
45
|
html.Div(
|
|
50
46
|
"Password",
|
|
51
|
-
style={
|
|
47
|
+
style={
|
|
48
|
+
"margin": 5,
|
|
49
|
+
"marginTop": 15,
|
|
50
|
+
"fontWeight": "bold",
|
|
51
|
+
},
|
|
52
52
|
),
|
|
53
53
|
dbc.Input(
|
|
54
54
|
id=f"{store_id}=password",
|
|
@@ -60,7 +60,11 @@ def get_header(
|
|
|
60
60
|
"Login failed.",
|
|
61
61
|
hidden=True,
|
|
62
62
|
id=f"{store_id}=login-failed",
|
|
63
|
-
style={
|
|
63
|
+
style={
|
|
64
|
+
"color": "red",
|
|
65
|
+
"fontWeight": "bold",
|
|
66
|
+
"margin": 10,
|
|
67
|
+
},
|
|
64
68
|
),
|
|
65
69
|
],
|
|
66
70
|
),
|
|
@@ -6,6 +6,7 @@ import pandas as pd
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
import cv2 as cv
|
|
8
8
|
from copy import deepcopy
|
|
9
|
+
from geopandas import GeoDataFrame
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def get_item_large_image_metadata(gc: GirderClient, item_id: str) -> dict:
|
|
@@ -87,19 +88,19 @@ def get_region(
|
|
|
87
88
|
height: int,
|
|
88
89
|
mag: float | None = None,
|
|
89
90
|
) -> np.ndarray:
|
|
90
|
-
"""Get
|
|
91
|
-
|
|
92
|
-
the image size.
|
|
91
|
+
"""Get image region from DSA. Note that the output image might not
|
|
92
|
+
be in the shape (width, height) if left + width or top + height
|
|
93
|
+
exceeds the image size.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
gc (girder_client.GirderClient): The authenticated girder client.
|
|
97
|
+
item_id (str): The item id.
|
|
93
98
|
left (int): The left coordinate.
|
|
94
99
|
top (int): The top coordinate.
|
|
95
100
|
width (int): The width of the region.
|
|
96
101
|
height (int): The height of the region.
|
|
97
|
-
mag (float, optional): The magnification. Defaults to None which
|
|
98
|
-
the image at scan magnification.
|
|
99
|
-
magnification will result in an ouptut image smaller than the
|
|
100
|
-
width and height. Similarly, using a mag higher than the scan
|
|
101
|
-
magnification will result in an output image larger than the width
|
|
102
|
-
and height.
|
|
102
|
+
mag (float, optional): The magnification. Defaults to None which
|
|
103
|
+
returns the image at the scan magnification.
|
|
103
104
|
|
|
104
105
|
Returns:
|
|
105
106
|
np.ndarray: The region of the image.
|
|
@@ -257,17 +258,16 @@ def _rotate_point_list(point_list, rotation, center=(0, 0)):
|
|
|
257
258
|
return point_list_rotated
|
|
258
259
|
|
|
259
260
|
|
|
260
|
-
def get_rectangle_element_coords(element):
|
|
261
|
+
def get_rectangle_element_coords(element: dict) -> np.ndarray:
|
|
261
262
|
"""Get the corner coordinate from a rectangle HistomicsUI element, can handle rotated elements.
|
|
262
263
|
Adapted from: https://github.com/DigitalSlideArchive/HistomicsTK/blob/master/histomicstk/annotations_and_masks/annotation_and_mask_utils.py
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
array of shape [4, 2] for the four corners of the rectangle in (x, y) format
|
|
264
|
+
|
|
265
|
+
Args:
|
|
266
|
+
element (dict): rectangle element from HistomicsUI annotation.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
corner_coords (np.ndarray): 4x2 array of corner coordinates of rectangle.
|
|
270
|
+
|
|
271
271
|
"""
|
|
272
272
|
# element is a dict so prevent referencing
|
|
273
273
|
element = deepcopy(element)
|
|
@@ -400,7 +400,9 @@ def get_roi_with_yolo_labels_from_single_doc(
|
|
|
400
400
|
if element.get("group") == roi_element_group:
|
|
401
401
|
# Append the ROI element, there can't more than one.
|
|
402
402
|
if roi_element is not None:
|
|
403
|
-
raise ValueError(
|
|
403
|
+
raise ValueError(
|
|
404
|
+
"Multiple ROI elements found in the document."
|
|
405
|
+
)
|
|
404
406
|
|
|
405
407
|
roi_element = element
|
|
406
408
|
elif element.get("group") in group_map:
|
|
@@ -459,7 +461,9 @@ def get_roi_with_yolo_labels_from_single_doc(
|
|
|
459
461
|
for element in box_elements:
|
|
460
462
|
if element.get("group") in group_map:
|
|
461
463
|
if element.get("type") != "rectangle":
|
|
462
|
-
print(
|
|
464
|
+
print(
|
|
465
|
+
f"Skipping element: {element.get('type')} not supported."
|
|
466
|
+
)
|
|
463
467
|
continue
|
|
464
468
|
|
|
465
469
|
group_id = group_map[element.get("group")]
|
|
@@ -487,3 +491,88 @@ def get_roi_with_yolo_labels_from_single_doc(
|
|
|
487
491
|
|
|
488
492
|
# Return the ROI image and labels data.
|
|
489
493
|
return img, labels_data.strip()
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def post_annotations_from_gdf(
|
|
497
|
+
gc: GirderClient,
|
|
498
|
+
item_id: str,
|
|
499
|
+
doc_name: str,
|
|
500
|
+
gdf: GeoDataFrame,
|
|
501
|
+
idx_config: dict,
|
|
502
|
+
tolerance: float = 0.5,
|
|
503
|
+
max_points: int = 1000,
|
|
504
|
+
) -> dict:
|
|
505
|
+
"""Post annotations from a GeoDataFrame to the DSA.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
gc (girder_client.GirderClient): The authenticated girder client.
|
|
509
|
+
item_id (str): The item id to post the annotations.
|
|
510
|
+
doc_name (str): The name of the document.
|
|
511
|
+
gdf (GeoDataFrame): The GeoDataFrame with the polygons.
|
|
512
|
+
idx_config (dict): For each label in the GeoDataFrame, the configuration
|
|
513
|
+
for the DSA annotation. Include "label", "fillColor", "lineColor", and
|
|
514
|
+
"group".
|
|
515
|
+
tolerance (float, optional): The tolerance to simplify the polygons.
|
|
516
|
+
Defaults to 0.5.
|
|
517
|
+
max_points (int, optional): If polygon has more than max_points,
|
|
518
|
+
simplify the polygon. Defaults to 1000.
|
|
519
|
+
|
|
520
|
+
Returns:
|
|
521
|
+
dict: The response from the DSA.
|
|
522
|
+
|
|
523
|
+
"""
|
|
524
|
+
elements = []
|
|
525
|
+
|
|
526
|
+
# Process the dataframe.
|
|
527
|
+
for _, row in gdf.iterrows():
|
|
528
|
+
# Flatten multipolygon to list of polygons.
|
|
529
|
+
list_of_polygons = list(
|
|
530
|
+
row["geometry"].geoms
|
|
531
|
+
) # assuming multipolygons
|
|
532
|
+
label = row["label"]
|
|
533
|
+
|
|
534
|
+
for poly in list_of_polygons:
|
|
535
|
+
exterior_poly = list(poly.exterior.coords)
|
|
536
|
+
interior_polys = [
|
|
537
|
+
list(interior.coords) for interior in poly.interiors
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
if len(exterior_poly) > max_points:
|
|
541
|
+
poly = poly.simplify(tolerance, preserve_topology=True)
|
|
542
|
+
exterior_poly = list(poly.exterior.coords)
|
|
543
|
+
|
|
544
|
+
points = [
|
|
545
|
+
[int(xy[0]) * 2, int(xy[1]) * 2, 0] for xy in exterior_poly
|
|
546
|
+
]
|
|
547
|
+
|
|
548
|
+
holes = []
|
|
549
|
+
|
|
550
|
+
for interior_poly in interior_polys:
|
|
551
|
+
hole = [
|
|
552
|
+
[int(xy[0]) * 2, int(xy[1]) * 2, 0] for xy in interior_poly
|
|
553
|
+
]
|
|
554
|
+
holes.append(hole)
|
|
555
|
+
|
|
556
|
+
element = {
|
|
557
|
+
"points": points,
|
|
558
|
+
"fillColor": idx_config[label]["fillColor"],
|
|
559
|
+
"lineColor": idx_config[label]["lineColor"],
|
|
560
|
+
"type": "polyline",
|
|
561
|
+
"lineWidth": 2,
|
|
562
|
+
"closed": True,
|
|
563
|
+
"label": {"value": idx_config[label]["label"]},
|
|
564
|
+
"group": idx_config[label]["group"],
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if len(holes):
|
|
568
|
+
element["holes"] = holes
|
|
569
|
+
|
|
570
|
+
elements.append(element)
|
|
571
|
+
|
|
572
|
+
response = gc.post(
|
|
573
|
+
"/annotation",
|
|
574
|
+
parameters={"itemId": item_id},
|
|
575
|
+
json={"name": doc_name, "description": "", "elements": elements},
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
return response
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Metric functions.
|
|
3
|
+
|
|
4
|
+
This module contains functions used to evaluate the performance of
|
|
5
|
+
machine learning models.
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
import torch
|
|
9
|
+
from torch import nn
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def binary_dice_coefficient(eval_pred: tuple[np.ndarray, np.ndarray]) -> dict:
|
|
14
|
+
"""Calculate the dice coefficient for binary masks. Developed for use
|
|
15
|
+
in compute_metrics parameters of transformers Trainer class, for
|
|
16
|
+
SegFormer senantic segmentation model of two classes.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
eval_pred (tuple[np.ndarray, np.ndarray]): A tuple containing the
|
|
20
|
+
predicted and true masks.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
dict: A dictionary containing the dice coefficient.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
with torch.no_grad():
|
|
27
|
+
logits, labels = eval_pred
|
|
28
|
+
|
|
29
|
+
# Convert to tensors.
|
|
30
|
+
logits = torch.from_numpy(logits).cpu()
|
|
31
|
+
|
|
32
|
+
# Resize the logits to be the same shape as the labels.
|
|
33
|
+
logits = nn.functional.interpolate(
|
|
34
|
+
logits,
|
|
35
|
+
size=labels.shape[-2:],
|
|
36
|
+
mode="bilinear",
|
|
37
|
+
align_corners=False,
|
|
38
|
+
).argmax(dim=1)
|
|
39
|
+
|
|
40
|
+
preds = logits.detach().numpy()
|
|
41
|
+
|
|
42
|
+
# Convert the masks to binary.
|
|
43
|
+
preds = preds > 0
|
|
44
|
+
labels = labels > 0
|
|
45
|
+
|
|
46
|
+
# Calculate dice.
|
|
47
|
+
intersection = np.sum(preds * labels)
|
|
48
|
+
union = np.sum(preds) + np.sum(labels)
|
|
49
|
+
|
|
50
|
+
if union == 0:
|
|
51
|
+
dice = 1.0 # Both masks are empty
|
|
52
|
+
else:
|
|
53
|
+
dice = 2.0 * intersection / union
|
|
54
|
+
|
|
55
|
+
return {"dice_coefficient": dice}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def mean_iou(eval_pred: tuple[np.ndarray, np.ndarray]) -> dict:
|
|
59
|
+
"""Compute the mean intersection over union (IoU) for semantic
|
|
60
|
+
segmentation. Also returns the IoU for each class.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
eval_pred (tuple[numpy.ndarray, numpy.ndarray]): Tuple of
|
|
64
|
+
logits and labels.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
dict: Dictionary of metrics.
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
with torch.no_grad():
|
|
71
|
+
logits, labels = eval_pred
|
|
72
|
+
|
|
73
|
+
# Convert logits to tensor.
|
|
74
|
+
logits_tensor = torch.from_numpy(logits).cpu()
|
|
75
|
+
labels_tensor = torch.from_numpy(labels).cpu()
|
|
76
|
+
|
|
77
|
+
# From logits get the number of classes in the dataset.
|
|
78
|
+
num_classes = logits.shape[1]
|
|
79
|
+
|
|
80
|
+
# Scale the logits back to the shape of the labels.
|
|
81
|
+
logits_tensor = nn.functional.interpolate(
|
|
82
|
+
logits_tensor,
|
|
83
|
+
size=labels.shape[-2:],
|
|
84
|
+
mode="bilinear",
|
|
85
|
+
align_corners=False,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Turn the logits to predictions.
|
|
89
|
+
pred = logits_tensor.argmax(dim=1)
|
|
90
|
+
|
|
91
|
+
# Calculate the IoU for each class.
|
|
92
|
+
ious = []
|
|
93
|
+
|
|
94
|
+
metrics = {}
|
|
95
|
+
|
|
96
|
+
for cls in range(num_classes):
|
|
97
|
+
pred_mask = pred == cls
|
|
98
|
+
labels_mask = labels_tensor == cls
|
|
99
|
+
|
|
100
|
+
intersection = (
|
|
101
|
+
torch.logical_and(pred_mask, labels_mask).sum().item()
|
|
102
|
+
)
|
|
103
|
+
union = torch.logical_or(pred_mask, labels_mask).sum().item()
|
|
104
|
+
|
|
105
|
+
if union == 0:
|
|
106
|
+
iou = float("nan") # or 1.0
|
|
107
|
+
else:
|
|
108
|
+
iou = intersection / union
|
|
109
|
+
|
|
110
|
+
metrics[f"label {cls} IoU"] = iou
|
|
111
|
+
|
|
112
|
+
ious.append(iou)
|
|
113
|
+
|
|
114
|
+
mean_iou = np.nanmean(ious) # Use NumPy for NaN handling
|
|
115
|
+
metrics["mean_iou"] = mean_iou
|
|
116
|
+
|
|
117
|
+
return metrics
|
{dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py
RENAMED
|
@@ -10,7 +10,7 @@ import torch
|
|
|
10
10
|
import torch.nn as nn
|
|
11
11
|
|
|
12
12
|
from ... import imread
|
|
13
|
-
from ...
|
|
13
|
+
from ...tiling import tile_image
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def inference(
|
|
@@ -115,7 +115,9 @@ def inference(
|
|
|
115
115
|
predicted_mask = predicted_mask[:h, :w]
|
|
116
116
|
|
|
117
117
|
# Extract the contours of the mask.
|
|
118
|
-
contours = cv.findContours(
|
|
118
|
+
contours = cv.findContours(
|
|
119
|
+
predicted_mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE
|
|
120
|
+
)[0]
|
|
119
121
|
|
|
120
122
|
# Return the mask.
|
|
121
123
|
return predicted_mask, contours
|
|
@@ -82,7 +82,7 @@ def _process_tile(
|
|
|
82
82
|
imwrite(img_fp, tile_img)
|
|
83
83
|
imwrite(mask_dir / fn, tile_mask, grayscale=True)
|
|
84
84
|
|
|
85
|
-
return str(img_fp)
|
|
85
|
+
return str(img_fp), x, y
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def semantic_seg_tile_wsi_with_dsa_annotations(
|
|
@@ -112,7 +112,8 @@ def semantic_seg_tile_wsi_with_dsa_annotations(
|
|
|
112
112
|
nproc (int, optional): number of processes to use for tiling.
|
|
113
113
|
|
|
114
114
|
Returns:
|
|
115
|
-
list[str]: list of
|
|
115
|
+
list[str]: A list of tuples: (tile file path, x, y coordinates
|
|
116
|
+
of tile).
|
|
116
117
|
|
|
117
118
|
"""
|
|
118
119
|
# Format annotation documents for drawing on tile masks.
|
|
@@ -216,9 +217,9 @@ def semantic_seg_tile_wsi_with_dsa_annotations(
|
|
|
216
217
|
for xy in xys
|
|
217
218
|
]
|
|
218
219
|
|
|
219
|
-
|
|
220
|
+
tile_info = []
|
|
220
221
|
|
|
221
222
|
for job in tqdm(jobs, desc="Tiling..."):
|
|
222
|
-
|
|
223
|
+
tile_info.append(job.get())
|
|
223
224
|
|
|
224
|
-
return
|
|
225
|
+
return tile_info
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Tile functions.
|
|
3
|
+
|
|
4
|
+
This module contains functions used when working tiles.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
from geopandas import GeoDataFrame
|
|
8
|
+
from .utils import mask_to_shapely
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def merge_tile_masks(tile_list: list, buffer: int = 1) -> GeoDataFrame:
|
|
12
|
+
"""
|
|
13
|
+
Merge the tile masks into a single mask for a large image.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
tile_list (list): List of three-lenght tuples containing:
|
|
17
|
+
(1) fp or array for mask, (2) x-coordinate for tile, (3)
|
|
18
|
+
y-coordinate for tile.
|
|
19
|
+
buffer (int): Buffer to apply to the polygons before dissolving.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
GeoDataFrame: GeoDataFrame with the merged mask.
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
polygons_and_labels = []
|
|
26
|
+
|
|
27
|
+
# Process each tile.
|
|
28
|
+
for tile_info in tile_list:
|
|
29
|
+
tile, x, y = tile_info
|
|
30
|
+
|
|
31
|
+
# Process the mask by converting it to polygons.
|
|
32
|
+
polygons_and_labels.extend(
|
|
33
|
+
mask_to_shapely(tile, x_offset=x, y_offset=y)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Convert polygons and labels into a GeoDataFrame.
|
|
37
|
+
gdf = GeoDataFrame(polygons_and_labels, columns=["geometry", "label"])
|
|
38
|
+
|
|
39
|
+
# Apply a buffer to make edges touch.
|
|
40
|
+
gdf["geometry"] = gdf["geometry"].buffer(buffer)
|
|
41
|
+
|
|
42
|
+
# Dissolve the dataframe by the label.
|
|
43
|
+
gdf = gdf.dissolve(by="label", as_index=False)
|
|
44
|
+
|
|
45
|
+
# Remove the buffer.
|
|
46
|
+
gdf["geometry"] = gdf["geometry"].buffer(-buffer)
|
|
47
|
+
|
|
48
|
+
return gdf
|