dsa-helpers 0.8.dev1__tar.gz → 0.9.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.
Files changed (49) hide show
  1. {dsa_helpers-0.8.dev1/dsa_helpers.egg-info → dsa_helpers-0.9.dev1}/PKG-INFO +5 -3
  2. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/README.md +3 -0
  3. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/__init__.py +11 -2
  4. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/girder_utils.py +84 -0
  5. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/__init__.py +0 -1
  6. dsa_helpers-0.9.dev1/dsa_helpers/ml/metrics.py +117 -0
  7. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/tiling/semantic_seg_tile_wsi_with_dsa_annotations.py +6 -5
  8. dsa_helpers-0.9.dev1/dsa_helpers/tile_utils.py +48 -0
  9. dsa_helpers-0.9.dev1/dsa_helpers/tiling.py +340 -0
  10. dsa_helpers-0.9.dev1/dsa_helpers/utils.py +75 -0
  11. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1/dsa_helpers.egg-info}/PKG-INFO +5 -3
  12. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers.egg-info/SOURCES.txt +3 -4
  13. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers.egg-info/requires.txt +1 -2
  14. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/pyproject.toml +2 -3
  15. dsa_helpers-0.9.dev1/tests/test.ipynb +141 -0
  16. dsa_helpers-0.8.dev1/dsa_helpers/image_utils.py +0 -74
  17. dsa_helpers-0.8.dev1/dsa_helpers/ml/metrics/__init__.py +0 -3
  18. dsa_helpers-0.8.dev1/dsa_helpers/ml/metrics/binary_dice_coefficient_metric.py +0 -24
  19. dsa_helpers-0.8.dev1/dsa_helpers/utils.py +0 -28
  20. dsa_helpers-0.8.dev1/requirements.txt +0 -23
  21. dsa_helpers-0.8.dev1/tests/test.ipynb +0 -209
  22. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/.gitignore +0 -0
  23. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/LICENSE +0 -0
  24. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/app.py +0 -0
  25. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/dash/__init__.py +0 -0
  26. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
  27. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/dash/header.py +0 -0
  28. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/dash/header_callbacks.py +0 -0
  29. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/imread.py +0 -0
  30. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/imwrite.py +0 -0
  31. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
  32. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/datasets/__init__.py +0 -0
  33. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/datasets/utils.py +0 -0
  34. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/object_detection/__init__.py +0 -0
  35. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
  36. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
  37. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/object_detection/utils.py +0 -0
  38. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
  39. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -0
  40. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/tiling/__init__.py +0 -0
  41. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/transforms/__init__.py +0 -0
  42. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
  43. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/mongo_utils.py +0 -0
  44. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers.egg-info/dependency_links.txt +0 -0
  45. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers.egg-info/top_level.txt +0 -0
  46. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/setup.cfg +0 -0
  47. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/tests/app/Dockerfile +0 -0
  48. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/tests/app/app.py +0 -0
  49. {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/tests/docker-compose.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dsa_helpers
3
- Version: 0.8.dev1
3
+ Version: 0.9.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>
@@ -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
  ```
@@ -1,16 +1,25 @@
1
1
  # Shadow imports.
2
2
  from .imread import imread
3
3
  from .imwrite import imwrite
4
- from . import girder_utils, image_utils, dash, ml, mongo_utils, utils
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
5
13
 
6
14
  # Modules that should be available.
7
15
  __all__ = [
8
16
  "girder_utils",
9
- "image_utils",
10
17
  "dash",
11
18
  "ml",
12
19
  "imread",
13
20
  "imwrite",
14
21
  "mongo_utils",
22
+ "tiling",
15
23
  "utils",
24
+ "tile_utils",
16
25
  ]
@@ -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:
@@ -487,3 +488,86 @@ def get_roi_with_yolo_labels_from_single_doc(
487
488
 
488
489
  # Return the ROI image and labels data.
489
490
  return img, labels_data.strip()
491
+
492
+
493
+ def post_annotations_from_gdf(
494
+ gc: GirderClient,
495
+ item_id: str,
496
+ doc_name: str,
497
+ gdf: GeoDataFrame,
498
+ idx_config: dict,
499
+ tolerance: float = 0.5,
500
+ max_points: int = 1000,
501
+ ) -> dict:
502
+ """Post annotations from a GeoDataFrame to the DSA.
503
+
504
+ Args:
505
+ gc (girder_client.GirderClient): The authenticated girder client.
506
+ item_id (str): The item id to post the annotations.
507
+ doc_name (str): The name of the document.
508
+ gdf (GeoDataFrame): The GeoDataFrame with the polygons.
509
+ idx_config (dict): For each label in the GeoDataFrame, the configuration
510
+ for the DSA annotation. Include "label", "fillColor", "lineColor", and
511
+ "group".
512
+ tolerance (float, optional): The tolerance to simplify the polygons.
513
+ Defaults to 0.5.
514
+ max_points (int, optional): If polygon has more than max_points,
515
+ simplify the polygon. Defaults to 1000.
516
+
517
+ Returns:
518
+ dict: The response from the DSA.
519
+
520
+ """
521
+ elements = []
522
+
523
+ # Process the dataframe.
524
+ for _, row in gdf.iterrows():
525
+ # Flatten multipolygon to list of polygons.
526
+ list_of_polygons = list(row["geometry"].geoms) # assuming multipolygons
527
+ label = row["label"]
528
+
529
+ for poly in list_of_polygons:
530
+ exterior_poly = list(poly.exterior.coords)
531
+ interior_polys = [
532
+ list(interior.coords) for interior in poly.interiors
533
+ ]
534
+
535
+ if len(exterior_poly) > max_points:
536
+ poly = poly.simplify(tolerance, preserve_topology=True)
537
+ exterior_poly = list(poly.exterior.coords)
538
+
539
+ points = [
540
+ [int(xy[0]) * 2, int(xy[1]) * 2, 0] for xy in exterior_poly
541
+ ]
542
+
543
+ holes = []
544
+
545
+ for interior_poly in interior_polys:
546
+ hole = [
547
+ [int(xy[0]) * 2, int(xy[1]) * 2, 0] for xy in interior_poly
548
+ ]
549
+ holes.append(hole)
550
+
551
+ element = {
552
+ "points": points,
553
+ "fillColor": idx_config[label]["fillColor"],
554
+ "lineColor": idx_config[label]["lineColor"],
555
+ "type": "polyline",
556
+ "lineWidth": 2,
557
+ "closed": True,
558
+ "label": {"value": idx_config[label]["label"]},
559
+ "group": idx_config[label]["group"],
560
+ }
561
+
562
+ if len(holes):
563
+ element["holes"] = holes
564
+
565
+ elements.append(element)
566
+
567
+ response = gc.post(
568
+ "/annotation",
569
+ parameters={"itemId": item_id},
570
+ json={"name": doc_name, "description": "", "elements": elements},
571
+ )
572
+
573
+ 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,
@@ -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
@@ -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