dsa-helpers 0.3__tar.gz → 0.3.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 (30) hide show
  1. {dsa_helpers-0.3/dsa_helpers.egg-info → dsa_helpers-0.3.dev1}/PKG-INFO +1 -3
  2. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/__init__.py +2 -3
  3. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/girder_utils.py +0 -34
  4. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1/dsa_helpers.egg-info}/PKG-INFO +1 -3
  5. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/SOURCES.txt +2 -8
  6. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/requires.txt +0 -2
  7. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/pyproject.toml +2 -4
  8. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/requirements.txt +1 -2
  9. dsa_helpers-0.3/app.py +0 -14
  10. dsa_helpers-0.3/dsa_helpers/ml/__init__.py +0 -2
  11. dsa_helpers-0.3/dsa_helpers/ml/object_detection/__init__.py +0 -5
  12. dsa_helpers-0.3/dsa_helpers/ml/object_detection/get_pearce_roi_images.py +0 -136
  13. dsa_helpers-0.3/dsa_helpers/ml/object_detection/tile_image.py +0 -137
  14. dsa_helpers-0.3/dsa_helpers/ml/object_detection/utils.py +0 -104
  15. dsa_helpers-0.3/test.ipynb +0 -268
  16. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/.gitignore +0 -0
  17. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/LICENSE +0 -0
  18. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/README.md +0 -0
  19. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/dash/__init__.py +0 -0
  20. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/dash/header.py +0 -0
  21. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/dash/header_callbacks.py +0 -0
  22. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/image_utils.py +0 -0
  23. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/imread.py +0 -0
  24. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/imwrite.py +0 -0
  25. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/login.py +0 -0
  26. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers/mongo_utils.py +0 -0
  27. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/dependency_links.txt +0 -0
  28. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/dsa_helpers.egg-info/top_level.txt +0 -0
  29. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/readthedocs.yaml +0 -0
  30. {dsa_helpers-0.3 → dsa_helpers-0.3.dev1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dsa_helpers
3
- Version: 0.3
3
+ Version: 0.3.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>
@@ -20,8 +20,6 @@ Requires-Dist: pymongo
20
20
  Requires-Dist: pillow
21
21
  Requires-Dist: dash
22
22
  Requires-Dist: dash_bootstrap_components
23
- Requires-Dist: shapely
24
- Requires-Dist: geopandas
25
23
 
26
24
  # Digital Slide Archive (DSA) Helpers
27
25
  Digital Slide Archive Python utility library.
@@ -1,8 +1,7 @@
1
1
  # Shadow imports.
2
+ from .login import login
2
3
  from .imread import imread
3
4
  from .imwrite import imwrite
4
5
 
5
6
  # Modules that should be available.
6
- # from . import girder_utils, image_utils, dash
7
-
8
- __all__ = ["girder_utils", "image_utils", "dash"]
7
+ from . import girder_utils, image_utils, dash
@@ -285,37 +285,3 @@ 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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dsa_helpers
3
- Version: 0.3
3
+ Version: 0.3.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>
@@ -20,8 +20,6 @@ Requires-Dist: pymongo
20
20
  Requires-Dist: pillow
21
21
  Requires-Dist: dash
22
22
  Requires-Dist: dash_bootstrap_components
23
- Requires-Dist: shapely
24
- Requires-Dist: geopandas
25
23
 
26
24
  # Digital Slide Archive (DSA) Helpers
27
25
  Digital Slide Archive Python utility library.
@@ -1,11 +1,9 @@
1
1
  .gitignore
2
2
  LICENSE
3
3
  README.md
4
- app.py
5
4
  pyproject.toml
6
5
  readthedocs.yaml
7
6
  requirements.txt
8
- test.ipynb
9
7
  dsa_helpers/__init__.py
10
8
  dsa_helpers/girder_utils.py
11
9
  dsa_helpers/image_utils.py
@@ -13,6 +11,7 @@ dsa_helpers/imread.py
13
11
  dsa_helpers/imwrite.py
14
12
  dsa_helpers/login.py
15
13
  dsa_helpers/mongo_utils.py
14
+ dsa_helpers/object_detection.py
16
15
  dsa_helpers.egg-info/PKG-INFO
17
16
  dsa_helpers.egg-info/SOURCES.txt
18
17
  dsa_helpers.egg-info/dependency_links.txt
@@ -20,9 +19,4 @@ dsa_helpers.egg-info/requires.txt
20
19
  dsa_helpers.egg-info/top_level.txt
21
20
  dsa_helpers/dash/__init__.py
22
21
  dsa_helpers/dash/header.py
23
- dsa_helpers/dash/header_callbacks.py
24
- dsa_helpers/ml/__init__.py
25
- dsa_helpers/ml/object_detection/__init__.py
26
- dsa_helpers/ml/object_detection/get_pearce_roi_images.py
27
- dsa_helpers/ml/object_detection/tile_image.py
28
- dsa_helpers/ml/object_detection/utils.py
22
+ dsa_helpers/dash/header_callbacks.py
@@ -7,5 +7,3 @@ pymongo
7
7
  pillow
8
8
  dash
9
9
  dash_bootstrap_components
10
- shapely
11
- geopandas
@@ -12,9 +12,7 @@ dependencies = [
12
12
  'pymongo',
13
13
  'pillow',
14
14
  'dash',
15
- 'dash_bootstrap_components',
16
- 'shapely',
17
- 'geopandas'
15
+ 'dash_bootstrap_components'
18
16
  ]
19
17
  name = "dsa_helpers"
20
18
  authors = [
@@ -31,7 +29,7 @@ classifiers = [
31
29
  "License :: OSI Approved :: MIT License",
32
30
  ]
33
31
  keywords = ["dsa", "girder", "girder_client"]
34
- version = "v0.3"
32
+ version = "v0.3dev1"
35
33
 
36
34
  [project.urls]
37
35
  Repository = "https://github.com/Gutman-Lab/dsa-python-utils"
@@ -7,5 +7,4 @@ pymongo
7
7
  jupyter
8
8
  pillow
9
9
  dash
10
- dash_bootstrap_components
11
- python-dotenv
10
+ dash_bootstrap_components
dsa_helpers-0.3/app.py DELETED
@@ -1,14 +0,0 @@
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,2 +0,0 @@
1
- # Modules that can be imported.
2
- __all__ = ["object_detection"]
@@ -1,5 +0,0 @@
1
- # Shadow import functions.
2
- from .get_pearce_roi_images import get_pearce_roi_images
3
- from .tile_image import tile_image
4
-
5
- __all__ = ["utils"]
@@ -1,136 +0,0 @@
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
- )
@@ -1,137 +0,0 @@
1
- import pandas as pd
2
- from geopandas import GeoDataFrame
3
- from pathlib import Path
4
- import cv2 as cv
5
- import numpy as np
6
-
7
- from ... import imread, imwrite
8
- from .utils import read_yolo_label, corners_to_polygon
9
-
10
-
11
- def tile_image(
12
- fp: str,
13
- label_fp: str | None = None,
14
- save_dir: str = ".",
15
- tile_size: int = 1280,
16
- stride: int = None,
17
- boundary_thr: float = 0.2,
18
- fill: tuple[int, int, int] = (114, 114, 114),
19
- box_thr: float = 0.2,
20
- ) -> pd.DataFrame:
21
- """Tile an ROI image with labels.
22
-
23
- Args:
24
- fp: Image filepath, should be in an '/images/ directory'.
25
- save_dir: Location to save images and labels.
26
- tile_size: Size of tile, uses square tiles only.
27
- stride: Stride to use when tiling, if None then it is set equal to
28
- tile_size (no overlap between tiles).
29
- boundary_thr: If ROI has a boundary (for rotated ROIs) then a tile must
30
- have sufficient area in boundary to be included (0.0 - 1.0).
31
- fill: RGB when padding image.
32
- box_thr: Area threshold of box that must be in a tile.
33
-
34
- Returns:
35
- Metadata of tiles saved.
36
-
37
- """
38
- # Read the image.
39
- img = imread(fp)
40
- h, w = img.shape[:2]
41
-
42
- # Get the name of the file.
43
- name = Path(fp).stem
44
-
45
- # Get the label if it exists.
46
- labels = []
47
-
48
- if label_fp is not None:
49
- labels = read_yolo_label(label_fp, im_shape=(w, h), convert=True)
50
-
51
- # Convert the labels into a GeoDataFrame.
52
- label_df = []
53
-
54
- for box in labels:
55
- label = box[0]
56
- x1, y1, x2, y2 = box[1:5].astype(int)
57
-
58
- label_df.append([label, x1, y1, x2, y2, corners_to_polygon(x1, y1, x2, y2)])
59
-
60
- label_df = GeoDataFrame(
61
- label_df, columns=["label", "x1", "y1", "x2", "y2", "geometry"]
62
- )
63
- label_areas = label_df.area
64
-
65
- # Depracated code for rotated ROIs.
66
- roi = corners_to_polygon(0, 0, w, h)
67
-
68
- # Make the directories to save images and label files.
69
- img_dir = Path(save_dir) / "images"
70
- img_dir.mkdir(parents=True, exist_ok=True)
71
-
72
- label_dir = Path(save_dir) / "labels"
73
- label_dir.mkdir(parents=True, exist_ok=True)
74
-
75
- # Default stride is no-overlap.
76
- if stride is None:
77
- stride = tile_size # default behavior - no overlap
78
-
79
- # Pad the image to avoid getting tiles not of the right size.
80
- img = cv.copyMakeBorder(
81
- img, 0, tile_size, 0, tile_size, cv.BORDER_CONSTANT, value=fill
82
- )
83
-
84
- # Get the top left corner of each tile.
85
- xys = list(((x, y) for x in range(0, w, stride) for y in range(0, h, stride)))
86
-
87
- tile_df = [] # track tile data.
88
-
89
- # Pre-calculate the number of pixels in tile that must be in ROI to include.
90
- intersection_thr = tile_size**2 * boundary_thr
91
-
92
- # loop through each tile coordinate
93
- for xy in xys:
94
- # Check if this tile is sufficiently in the boundary.
95
- x1, y1 = xy
96
- x2, y2 = x1 + tile_size, y1 + tile_size
97
-
98
- tile_pol = corners_to_polygon(x1, y1, x2, y2)
99
- intersection = roi.intersection(tile_pol).area
100
-
101
- if intersection > intersection_thr:
102
- # Get the tile image.
103
- tile = img[y1:y2, x1:x2]
104
-
105
- # Create a name for the tile image / label.
106
- fn = f"{name}_x{x1}y{y1}x{x2}y{y2}"
107
-
108
- img_fp = img_dir / f"{fn}.png"
109
-
110
- tile_df.append([img_fp, x1, y1, x2, y2])
111
-
112
- imwrite(str(img_fp), tile)
113
-
114
- # Find all boxes that intersect
115
- label_intersection = label_df.geometry.intersection(tile_pol).area
116
-
117
- tile_boxes = label_df[label_intersection / label_areas > box_thr]
118
-
119
- # save these as normalized labels, threshold the box edges.
120
- if len(tile_boxes):
121
- labels = ""
122
-
123
- for _, r in tile_boxes.iterrows():
124
- # Shift coordinates to be relative to this tile.
125
- xx1 = np.clip(r.x1 - x1, 0, tile_size) / tile_size
126
- yy1 = np.clip(r.y1 - y1, 0, tile_size) / tile_size
127
- xx2 = np.clip(r.x2 - x1, 0, tile_size) / tile_size
128
- yy2 = np.clip(r.y2 - y1, 0, tile_size) / tile_size
129
-
130
- xc, yc = (xx1 + xx2) / 2, (yy1 + yy2) / 2
131
- bw, bh = xx2 - xx1, yy2 - yy1
132
- labels += f"{r.label} {xc:.4f} {yc:.4f} {bw:.4f} {bh:.4f}\n"
133
-
134
- with open(str(label_dir / f"{fn}.txt"), "w") as fh:
135
- fh.write(labels.strip())
136
-
137
- return pd.DataFrame(tile_df, columns=["fp", "x1", "y1", "x2", "y2"])
@@ -1,104 +0,0 @@
1
- import numpy as np
2
- from shapely.geometry import Polygon
3
-
4
-
5
- def convert_box_type(box):
6
- """Convert a box type from YOLO format (x-center, y-center, box-width, box-height) to (x1, y1, x2, y2) where point 1 is the
7
- top left corner of box and point 2 is the bottom right corner
8
-
9
- INPUT
10
- -----
11
- box : array
12
- [N, 4], each row a point and the format being (x-center, y-center, box-width, box-height)
13
-
14
- RETURN
15
- ------
16
- new_box : array
17
- [N, 4] each row a point and the format x1, y1, x2, y2
18
-
19
- """
20
- # get half the box height and width
21
- half_bw = box[:, 2] / 2
22
- half_bh = box[:, 3] / 2
23
-
24
- new_box = np.zeros(box.shape, dtype=box.dtype)
25
- new_box[:, 0] = box[:, 0] - half_bw
26
- new_box[:, 1] = box[:, 1] - half_bh
27
- new_box[:, 2] = box[:, 0] + half_bw
28
- new_box[:, 3] = box[:, 1] + half_bh
29
-
30
- return new_box
31
-
32
-
33
- def read_yolo_label(filepath, im_shape=None, shift=None, convert=False):
34
- """Read a yolo label text file. It may contain a confidence value for the labels or not, will handle both cases
35
-
36
- INPUTS
37
- ------
38
- filepath : str
39
- the path of the text file
40
- im_shape : tuple or int (default: None)
41
- image width and height corresponding to the label, if an int it is assumed both are the same. Will scale coordinates
42
- to int values instead of normalized if given
43
- shift : tuple or int (default: None)
44
- shift value in the x and y direction, if int it is assumed to be the same in both. These values will be subtracted and applied
45
- after scaling if needed
46
- convert : bool (default: False)
47
- If True, convert the output boxes from yolo format (label, x-center, y-center, width, height, conf) to (label, x1, y1, x2, y2, conf)
48
- where point 1 is the top left corner of box and point 2 is the bottom corner of box
49
-
50
- RETURN
51
- ------
52
- coords : array
53
- coordinates array, [N, 4 or 5] depending if confidence was in input file
54
-
55
- """
56
- coords = []
57
-
58
- with open(filepath, "r") as fh:
59
- for line in fh.readlines():
60
- if len(line):
61
- coords.append([float(ln) for ln in line.strip().split(" ")])
62
-
63
- coords = np.array(coords)
64
-
65
- # scale coords if needed
66
- if im_shape is not None:
67
- if isinstance(im_shape, int):
68
- w, h = im_shape, im_shape
69
- else:
70
- w, h = im_shape[:2]
71
-
72
- coords[:, 1] *= w
73
- coords[:, 3] *= w
74
- coords[:, 2] *= h
75
- coords[:, 4] *= h
76
-
77
- # shift coords
78
- if shift is not None:
79
- if isinstance(shift, int):
80
- x_shift, y_shift = shift, shift
81
- else:
82
- x_shift, y_shift = shift[:2]
83
-
84
- coords[:, 1] -= x_shift
85
- coords[:, 2] -= y_shift
86
-
87
- if convert:
88
- coords[:, 1:5] = convert_box_type(coords[:, 1:5])
89
-
90
- return coords
91
-
92
-
93
- def corners_to_polygon(x1: int, y1: int, x2: int, y2: int) -> Polygon:
94
- """Return a Polygon from shapely with the box coordinates given the top left and bottom right corners of a
95
- rectangle (can be rotated).
96
-
97
- Args:
98
- x1, y1, x2, y2: Coordinates of the top left corner (point 1) and the bottom right corner (point 2) of a box.
99
-
100
- Returns:
101
- Shapely polygon object of the box.
102
-
103
- """
104
- return Polygon([(x1, y1), (x2, y1), (x2, y2), (x1, y2)])