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.
- {dsa_helpers-0.8.dev1/dsa_helpers.egg-info → dsa_helpers-0.9.dev1}/PKG-INFO +5 -3
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/README.md +3 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/__init__.py +11 -2
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/girder_utils.py +84 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/__init__.py +0 -1
- dsa_helpers-0.9.dev1/dsa_helpers/ml/metrics.py +117 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/tiling/semantic_seg_tile_wsi_with_dsa_annotations.py +6 -5
- dsa_helpers-0.9.dev1/dsa_helpers/tile_utils.py +48 -0
- dsa_helpers-0.9.dev1/dsa_helpers/tiling.py +340 -0
- dsa_helpers-0.9.dev1/dsa_helpers/utils.py +75 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1/dsa_helpers.egg-info}/PKG-INFO +5 -3
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers.egg-info/SOURCES.txt +3 -4
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers.egg-info/requires.txt +1 -2
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/pyproject.toml +2 -3
- dsa_helpers-0.9.dev1/tests/test.ipynb +141 -0
- 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.dev1}/.gitignore +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/LICENSE +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/app.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/dash/__init__.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/dash/dash_paperdragon_utils.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/dash/header.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/dash/header_callbacks.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/imread.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/imwrite.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/datasets/SegFormerSegmentationDataset.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/datasets/__init__.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/datasets/utils.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/object_detection/__init__.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/object_detection/tile_image.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/object_detection/utils.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/segformer_semantic_segmentation/__init__.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/segformer_semantic_segmentation/inference.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/tiling/__init__.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/transforms/__init__.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/ml/transforms/segformer_transforms.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers/mongo_utils.py +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers.egg-info/dependency_links.txt +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/dsa_helpers.egg-info/top_level.txt +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/setup.cfg +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/tests/app/Dockerfile +0 -0
- {dsa_helpers-0.8.dev1 → dsa_helpers-0.9.dev1}/tests/app/app.py +0 -0
- {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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|