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.
Files changed (54) hide show
  1. dsa_helpers-0.9/.readthedocs.yaml +16 -0
  2. {dsa_helpers-0.8.dev1/dsa_helpers.egg-info → dsa_helpers-0.9}/PKG-INFO +5 -3
  3. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/README.md +3 -0
  4. dsa_helpers-0.9/docs/requirements.txt +16 -0
  5. dsa_helpers-0.9/docs/source/conf.py +42 -0
  6. dsa_helpers-0.9/docs/source/index.rst +18 -0
  7. dsa_helpers-0.9/dsa_helpers/__init__.py +23 -0
  8. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/dash/header.py +14 -10
  9. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/girder_utils.py +109 -20
  10. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/__init__.py +0 -1
  11. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/datasets/__init__.py +1 -1
  12. dsa_helpers-0.9/dsa_helpers/ml/metrics.py +117 -0
  13. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/object_detection/__init__.py +1 -1
  14. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +1 -1
  15. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +4 -2
  16. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/tiling/__init__.py +1 -1
  17. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/tiling/semantic_seg_tile_wsi_with_dsa_annotations.py +6 -5
  18. dsa_helpers-0.9/dsa_helpers/tile_utils.py +48 -0
  19. dsa_helpers-0.9/dsa_helpers/tiling.py +340 -0
  20. dsa_helpers-0.9/dsa_helpers/utils.py +75 -0
  21. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9/dsa_helpers.egg-info}/PKG-INFO +5 -3
  22. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers.egg-info/SOURCES.txt +7 -4
  23. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers.egg-info/requires.txt +1 -2
  24. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/pyproject.toml +2 -3
  25. dsa_helpers-0.9/tests/test.ipynb +141 -0
  26. dsa_helpers-0.8.dev1/dsa_helpers/__init__.py +0 -16
  27. dsa_helpers-0.8.dev1/dsa_helpers/image_utils.py +0 -74
  28. dsa_helpers-0.8.dev1/dsa_helpers/ml/metrics/__init__.py +0 -3
  29. dsa_helpers-0.8.dev1/dsa_helpers/ml/metrics/binary_dice_coefficient_metric.py +0 -24
  30. dsa_helpers-0.8.dev1/dsa_helpers/utils.py +0 -28
  31. dsa_helpers-0.8.dev1/requirements.txt +0 -23
  32. dsa_helpers-0.8.dev1/tests/test.ipynb +0 -209
  33. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/.gitignore +0 -0
  34. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/LICENSE +0 -0
  35. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/app.py +0 -0
  36. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/dash/__init__.py +0 -0
  37. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
  38. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/dash/header_callbacks.py +0 -0
  39. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/imread.py +0 -0
  40. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/imwrite.py +0 -0
  41. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
  42. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/datasets/utils.py +0 -0
  43. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
  44. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
  45. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/object_detection/utils.py +0 -0
  46. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/transforms/__init__.py +0 -0
  47. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
  48. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers/mongo_utils.py +0 -0
  49. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers.egg-info/dependency_links.txt +0 -0
  50. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/dsa_helpers.egg-info/top_level.txt +0 -0
  51. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/setup.cfg +0 -0
  52. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/tests/app/Dockerfile +0 -0
  53. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9}/tests/app/app.py +0 -0
  54. {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.8.dev1
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: dash_bootstrap_components
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,16 @@
1
+ girder-client
2
+ numpy
3
+ setuptools
4
+ opencv-python
5
+ pandas
6
+ pymongo
7
+ pillow
8
+ dash
9
+ dash-bootstrap-components
10
+ shapely
11
+ geopandas
12
+ transformers[torch]
13
+ datasets
14
+ torchvision
15
+ large-image
16
+ sphinx_rtd_theme
@@ -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. Defaults to None.
19
- If None the default configuration is:
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", style={"margin": 5, "fontWeight": "bold"}
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={"margin": 5, "marginTop": 15, "fontWeight": "bold"},
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={"color": "red", "fontWeight": "bold", "margin": 10},
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 a region of the image for an item. Note that the output image might
91
- not be in the shape (width, height) if left + width or top + height exceeds
92
- the image size. The outpuzerositem id.
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 returns
98
- the image at scan magnification. Using a mag lower than the scan
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
- INPUTS
264
- ------
265
- element : dict
266
- rectangle element, in HistomicsUI format
267
- RETURN
268
- ------
269
- corner_coords : array
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("Multiple ROI elements found in the document.")
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(f"Skipping element: {element.get('type')} not supported.")
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
@@ -2,7 +2,6 @@
2
2
  from . import (
3
3
  object_detection,
4
4
  datasets,
5
- metrics,
6
5
  transforms,
7
6
  segformer_semantic_segmentation,
8
7
  tiling,
@@ -1,3 +1,3 @@
1
1
  from .SegFormerSegmentationDataset import SegFormerSegmentationDataset
2
2
 
3
- __all__ = ["SegFormerSegmentationDataset", "utils"]
3
+ __all__ = ["utils"]
@@ -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
@@ -2,4 +2,4 @@
2
2
  from .get_pearce_roi_images import get_pearce_roi_images
3
3
  from .tile_image import tile_image
4
4
 
5
- __all__ = ["utils", "get_pearce_roi_images", "tile_image"]
5
+ __all__ = ["utils"]
@@ -1,3 +1,3 @@
1
1
  from .inference import inference
2
2
 
3
- __all__ = ["inference"]
3
+ __all__ = []
@@ -10,7 +10,7 @@ import torch
10
10
  import torch.nn as nn
11
11
 
12
12
  from ... import imread
13
- from ...image_utils import tile_image
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(predicted_mask, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)[0]
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
@@ -2,4 +2,4 @@ from .semantic_seg_tile_wsi_with_dsa_annotations import (
2
2
  semantic_seg_tile_wsi_with_dsa_annotations,
3
3
  )
4
4
 
5
- __all__ = ["semantic_seg_tile_wsi_with_dsa_annotations"]
5
+ __all__ = []
@@ -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 file paths to the created tile images.
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
- fps = []
220
+ tile_info = []
220
221
 
221
222
  for job in tqdm(jobs, desc="Tiling..."):
222
- fps.append(job.get())
223
+ tile_info.append(job.get())
223
224
 
224
- return fps
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