Skip to content

API Reference

This page provides a comprehensive reference of the functions available in the PolyGoneNMS library.

polygone_nms.nms

apply_distributed_polygon_nms(polygons, nms_method, intersection_method, threshold=0.5, sigma=0.5)

Distributed version of apply_polygon_nms.

Parameters:

Name Type Description Default
polygons List[Tuple[Polygon, float, float]]

List of polygons, where each polygon is a tuple of the polygon represented by shapely.geometry.Polygon object, the class label, and the confidence score.

required
nms_method str

The NMS method to use, one of ("Default", "Soft", "Class Agnostic").

required
intersection_method Callable

The method to compute intersections.

required
threshold float

The threshold for the NMS method. Defaults to 0.5.

0.5
sigma float

The sigma for the Soft NMS method. Defaults to 0.5.

0.5

Returns:

Type Description
List[int]

List[int]: A list of kept polygon indices.

Source code in polygone_nms/nms.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def apply_distributed_polygon_nms(
    polygons: List[Tuple[Polygon, float, float]],
    nms_method: str,
    intersection_method: Callable,
    threshold: float = 0.5,
    sigma: float = 0.5,
) -> List[int]:
    """
    Distributed version of `apply_polygon_nms`.

    Args:
        polygons (List[Tuple[Polygon, float, float]]):
            List of polygons, where each polygon is a tuple of the polygon
            represented by shapely.geometry.Polygon object, the class label,
            and the confidence score.
        nms_method (str): The NMS method to use, one of
            ("Default", "Soft", "Class Agnostic").
        intersection_method (Callable): The method to compute intersections.
        threshold (float, optional): The threshold for the NMS method. Defaults to 0.5.
        sigma (float, optional): The sigma for the Soft NMS method. Defaults to 0.5.

    Returns:
        List[int]: A list of kept polygon indices.
    """
    return apply_polygon_nms(
        polygons, nms_method, intersection_method, threshold, sigma
    )

apply_polygon_nms(polygons, nms_method, intersection_method, threshold=0.5, sigma=0.5)

Apply Non-Maximum Suppression (NMS) to a list of predicted polygons.

Raises:

Type Description
ValueError

If the NMS method is invalid.

Parameters:

Name Type Description Default
polygons List[Tuple[Polygon, float, float]]

List of polygons, where each polygon is a tuple of the polygon represented by shapely.geometry.Polygon object, the class label, and the confidence score.

required
nms_method str

The NMS method to use, one of ("Default", "Soft", "Class Agnostic").

required
intersection_method Callable

The method to compute intersections.

required
threshold float

The threshold for the NMS method. Defaults to 0.5.

0.5
sigma float

The sigma for the Soft NMS method. Defaults to 0.5.

0.5

Returns:

Type Description
List[int]

List[int]: A list of kept polygon indices.

Source code in polygone_nms/nms.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def apply_polygon_nms(
    polygons: List[Tuple[Polygon, float, float]],
    nms_method: str,
    intersection_method: Callable,
    threshold: float = 0.5,
    sigma: float = 0.5,
) -> List[int]:
    """
    Apply Non-Maximum Suppression (NMS) to a list of predicted polygons.

    Raises:
        ValueError: If the NMS method is invalid.

    Args:
        polygons (List[Tuple[Polygon, float, float]]):
            List of polygons, where each polygon is a tuple of the polygon
            represented by shapely.geometry.Polygon object, the class label,
            and the confidence score.
        nms_method (str): The NMS method to use, one of
            ("Default", "Soft", "Class Agnostic").
        intersection_method (Callable): The method to compute intersections.
        threshold (float, optional): The threshold for the NMS method. Defaults to 0.5.
        sigma (float, optional): The sigma for the Soft NMS method. Defaults to 0.5.

    Returns:
        List[int]: A list of kept polygon indices.
    """
    # List of kept polygons
    kept_polygons = []

    # Sort polygons by confidence score
    confidences = np.array([poly[2] for poly in polygons])
    sorted_indices = np.argsort(confidences)[::-1]

    # Iterate over all sorted polygons by confidence score
    while sorted_indices.size > 0:
        # Get the index of the current polygon and the respective polygon and label
        i = sorted_indices[0]
        current_polygon = polygons[i][0]
        current_class = polygons[i][1]

        # Add the current polygon to the NMS kept polygons list
        kept_polygons.append(i)

        # Calculate the intersection between the current polygon and
        # all remaining polygons
        intersections = [
            intersection_method(current_polygon, polygons[j][0])
            for j in sorted_indices[1:]
        ]

        # If the NMS method is "Default", "Soft" or "Class Agnostic"
        if nms_method == "Default":
            # Remove all remaining polygons that have an intersection with
            # the current polygon greater than the threshold
            remaining_indices = [
                idx
                for idx, intersection in zip(sorted_indices[1:], intersections)
                if polygons[idx][1] != current_class or intersection <= threshold
            ]
        elif nms_method == "Soft":
            # Update the confidence scores of the remaining polygons
            for j, intersection in zip(sorted_indices[1:], intersections):
                # Calculate the weight for the current polygon equal to
                # e^(-intersection^2 / sigma)
                weight = np.exp(-(intersection**2) / sigma)
                confidences[j] *= weight

            # Discard polygons with confidence below the threshold
            remaining_indices = [
                idx for idx in sorted_indices[1:] if confidences[idx] >= threshold
            ]
        elif nms_method == "Class Agnostic":
            # Remove all remaining polygons that have an intersection with
            # the current polygon greater than the threshold and
            # have the same class label
            remaining_indices = [
                idx
                for idx, intersection in zip(sorted_indices[1:], intersections)
                if intersection <= threshold
            ]
        else:
            raise ValueError(
                (
                    f"Invalid NMS method: {nms_method}. "
                    f"Allowed methods are: {VALID_NMS_METHODS}"
                )
            )

        # Update the list of remaining polygon indices
        sorted_indices = np.array(remaining_indices)

    return kept_polygons

cluster_polygons(polygons, rtree_index)

Cluster polygons into non-overlapping subregions with R-Tree. Used for distributed computing.

Examples:

>>> from shapely.geometry import Polygon
>>> from polygone_nms.utils import build_rtree, cluster_polygons
>>> polygons = [
...     (Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), 0, 0.9),
...     (Polygon([(2, 0), (3, 0), (3, 1), (2, 1)]), 0, 0.9),
...     (Polygon([(4, 0), (5, 0), (5, 1), (4, 1)]), 0, 0.9),
...     (Polygon([(0, 0.5), (1, 0.5), (1, 2), (0, 2)]), 0, 0.9),
...     (Polygon([(2, 1), (3, 1), (3, 3), (2, 3)]), 0, 0.9),
... ]
>>> rtree = build_rtree(polygons)
>>> clustered_polygons = cluster_polygons(polygons, rtree)
>>> assert len(clustered_polygons) == 3
>>> assert sorted(clustered_polygons[0]) == [0, 3]
>>> assert sorted(clustered_polygons[1]) == [1, 4]

Parameters:

Name Type Description Default
polygons List[Tuple[Polygon, float, float]]

List of polygons, where each polygon is a tuple of the polygon, the class label, and the confidence score.

required
rtree_index rtree.index.Index

R-Tree index of the polygons.

required

Returns:

Type Description
List[List[Polygon]]

List[List[Polygon]]: A list of clusters, where each cluster is a list of non-overlapping polygons.

Source code in polygone_nms/nms.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def cluster_polygons(
    polygons: List[Tuple[Polygon, float, float]], rtree_index: rtree.index.Index
) -> List[List[Polygon]]:
    """
    Cluster polygons into non-overlapping subregions with R-Tree.
    Used for distributed computing.

    Examples:
        >>> from shapely.geometry import Polygon
        >>> from polygone_nms.utils import build_rtree, cluster_polygons
        >>> polygons = [
        ...     (Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), 0, 0.9),
        ...     (Polygon([(2, 0), (3, 0), (3, 1), (2, 1)]), 0, 0.9),
        ...     (Polygon([(4, 0), (5, 0), (5, 1), (4, 1)]), 0, 0.9),
        ...     (Polygon([(0, 0.5), (1, 0.5), (1, 2), (0, 2)]), 0, 0.9),
        ...     (Polygon([(2, 1), (3, 1), (3, 3), (2, 3)]), 0, 0.9),
        ... ]
        >>> rtree = build_rtree(polygons)
        >>> clustered_polygons = cluster_polygons(polygons, rtree)
        >>> assert len(clustered_polygons) == 3
        >>> assert sorted(clustered_polygons[0]) == [0, 3]
        >>> assert sorted(clustered_polygons[1]) == [1, 4]

    Args:
        polygons (List[Tuple[Polygon, float, float]]): List of polygons,
            where each polygon is a tuple of the polygon, the class label,
            and the confidence score.
        rtree_index (rtree.index.Index): R-Tree index of the polygons.

    Returns:
        List[List[Polygon]]:
            A list of clusters,
            where each cluster is a list of non-overlapping polygons.
    """
    # Create a list of adjacent polygons for each polygon and
    # a list of visited polygons
    adj_list: List[List[int]] = [[] for _ in range(len(polygons))]
    visited = [False] * len(polygons)

    # Iterate over all polygons
    for i, poly_tuple in enumerate(polygons):
        # Get the polygon
        poly = poly_tuple[0]
        # Get all polygons that intersect with the current polygon
        intersecting_polygons = list(rtree_index.intersection(poly.bounds))
        # Remove the current polygon from the list of intersecting polygons
        intersecting_polygons.remove(i)
        # Update the adjacency list
        adj_list[i] = intersecting_polygons

    # Create a list of clusters
    clusters = []
    # Iterate over all polygons
    for i in range(len(polygons)):
        # If the current polygon has not been visited
        if not visited[i]:
            # Perform a depth-first search to find all connected components
            # Due to RecursionError: maximum recursion depth exceeded
            # connected_component = dfs_recursive(i, visited, adj_list)
            connected_component = dfs_iterative(i, visited, adj_list)
            # # Create a cluster from the connected component
            # cluster = [polygons[j] for j in connected_component]
            # # Append the cluster to the list of clusters
            # Append the connected component (cluster) to the list of clusters
            clusters.append(connected_component)

    return clusters

nms(input_data, distributed=None, nms_method='Default', intersection_method='IOU', threshold=0.5, sigma=0.5, **kwargs)

Apply Non-Maximum Suppression (NMS) to a set of polygons. Method works with distributed computing for efficient processing and clustering.

Examples:

>>> import numpy as np
>>> from polygone.nms import nms
>>> input_data = np.array(
... [
...     [0.0, 0.0, 2.0, 0.0, 2.0, 2.0, 0.0, 2.0, 1.0, 0.9],
...     [1.0, 0.0, 3.0, 0.0, 3.0, 2.0, 1.0, 2.0, 1.0, 0.8],
...     [4.0, 4.0, 6.0, 4.0, 6.0, 6.0, 4.0, 6.0, 5.0, 0.95],
...     [10.0, 10.0, 12.0, 10.0, 12.0, 12.0, 10.0, 12.0, 11.0, 0.9],
...     [11.0, 10.0, 13.0, 10.0, 13.0, 12.0, 11.0, 12.0, 11.0, 0.8],
...     [14.0, 14.0, 16.0, 14.0, 16.0, 16.0, 14.0, 16.0, 15.0, 0.95],
... ])
>>> results = nms(input_data, None, "Default", "IOU", 0.3, 0.5)
>>> assert sorted(results) == [0, 2, 3, 5]

results = nms(input_data, None, nms_method, intersection_method, threshold, sigma) assert sorted(results) == expected

Parameters:

Name Type Description Default
input_data Union[np.ndarray, Tuple[Polygon, float, float]]

List of tuples of Polygon, class, score or numpy array of polygons. Each polygon in the numpy array is represented by a 1D array of n % 2 coordinates (x1, y1, x2, y2, .., x(n-1), y(n-1), class, score).

required
distributed Optional[str]

The distributed computing method to use, one of (None, "Ray", "Dask").. Defaults to None.

None
nms_method str

The NMS method to use, one of ("Default", "Soft", "Class Agnostic"). Defaults to "Default".

'Default'
intersection_method str

The method to compute intersections, one of ("IOU", "IOS", "Dice", "IOT"). Defaults to "IOU".

'IOU'
threshold float

The threshold for the NMS(intersection) method. Defaults to 0.5.

0.5
sigma float

The sigma for the Soft NMS method. Defaults to 0.5.

0.5
**kwargs

Additional arguments for the NMS method. Any keyword arguments for the distributed computing should be passed here.

{}

Returns:

Type Description
List[int]

List[int]: List of indices of the kept polygons.

Source code in polygone_nms/nms.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
def nms(
    input_data: Union[np.ndarray, Tuple[Polygon, float, float]],
    distributed: Optional[str] = None,
    nms_method: str = "Default",
    intersection_method: str = "IOU",
    threshold: float = 0.5,
    sigma: float = 0.5,
    **kwargs,
) -> List[int]:
    """
    Apply Non-Maximum Suppression (NMS) to a set of polygons.
    Method works with distributed computing for efficient processing and clustering.

    Examples:
        >>> import numpy as np
        >>> from polygone.nms import nms
        >>> input_data = np.array(
        ... [
        ...     [0.0, 0.0, 2.0, 0.0, 2.0, 2.0, 0.0, 2.0, 1.0, 0.9],
        ...     [1.0, 0.0, 3.0, 0.0, 3.0, 2.0, 1.0, 2.0, 1.0, 0.8],
        ...     [4.0, 4.0, 6.0, 4.0, 6.0, 6.0, 4.0, 6.0, 5.0, 0.95],
        ...     [10.0, 10.0, 12.0, 10.0, 12.0, 12.0, 10.0, 12.0, 11.0, 0.9],
        ...     [11.0, 10.0, 13.0, 10.0, 13.0, 12.0, 11.0, 12.0, 11.0, 0.8],
        ...     [14.0, 14.0, 16.0, 14.0, 16.0, 16.0, 14.0, 16.0, 15.0, 0.95],
        ... ])
        >>> results = nms(input_data, None, "Default", "IOU", 0.3, 0.5)
        >>> assert sorted(results) == [0, 2, 3, 5]

    results = nms(input_data, None, nms_method, intersection_method, threshold, sigma)
    assert sorted(results) == expected

    Args:
        input_data (Union[np.ndarray, Tuple[Polygon, float, float]]):
            List of tuples of Polygon, class, score or numpy array of polygons.
            Each polygon in the numpy array is represented by a 1D array of n % 2
            coordinates (x1, y1, x2, y2, .., x(n-1), y(n-1), class, score).
        distributed (Optional[str], optional):
            The distributed computing method to use,
            one of (None, "Ray", "Dask").. Defaults to None.
        nms_method (str, optional):
            The NMS method to use, one of ("Default", "Soft", "Class Agnostic").
            Defaults to "Default".
        intersection_method (str, optional):
            The method to compute intersections, one of ("IOU", "IOS", "Dice", "IOT").
            Defaults to "IOU".
        threshold (float, optional):
            The threshold for the NMS(intersection) method. Defaults to 0.5.
        sigma (float, optional):
            The sigma for the Soft NMS method. Defaults to 0.5.
        **kwargs: Additional arguments for the NMS method.
            Any keyword arguments for the distributed computing should be passed here.

    Returns:
        List[int]: List of indices of the kept polygons.
    """
    # Check if the input data is a list of polygons or a 2D NumPy array
    polygons = []
    if isinstance(input_data, list):
        if len(input_data) == 0:
            return []
        elif isinstance(input_data[0], tuple):
            polygons = input_data
        else:
            input_data = np.array(input_data)
    elif not isinstance(input_data, np.ndarray):
        raise ValueError(
            "Invalid input data type. Expected a list of polygons or a 2D NumPy array."
        )

    # Check distributed computing method
    if distributed is not None:
        if distributed not in VALID_DISTRIBUTED_METHODS:
            raise ValueError(
                (
                    f"Invalid distributed computing method: {distributed}. "
                    f"Allowed methods are: {VALID_DISTRIBUTED_METHODS}"
                )
            )

    # Check NMS method
    if nms_method not in VALID_NMS_METHODS:
        raise ValueError(
            (
                f"Invalid NMS method: {nms_method}. "
                f"Allowed methods are: {VALID_NMS_METHODS}"
            )
        )

    # Check intersection method
    if intersection_method not in list(INTERSECTION_METHODS.keys()):
        raise ValueError(
            (
                f"Invalid intersection method: {intersection_method}. "
                f"Allowed methods are: {list(INTERSECTION_METHODS.keys())}"
            )
        )

    # Get the intersection function
    intersection_func = INTERSECTION_METHODS[intersection_method]

    # Convert input data to Shapely Polygons and store class and confidence
    if len(polygons) == 0:
        for row in input_data:
            polygon_coords = row[:-2]
            class_label = row[-2]
            confidence = row[-1]
            polygon = create_polygon(polygon_coords)
            polygons.append((polygon, class_label, confidence))

    # Build R-Tree index
    rtree = build_rtree(polygons)

    # Cluster polygons into non-overlapping subregions
    clusters = cluster_polygons(polygons, rtree)
    polygon_clusters = [[polygons[idx] for idx in cluster] for cluster in clusters]

    # Apply NMS to the clustered polygons
    if distributed == "Ray":
        # Import Ray
        import ray

        # Check if Ray is initialized
        if ray.is_initialized():
            initialize_ray = False
        else:
            initialize_ray = True
            ray.init()  # Initialize Ray

        # Scatter the polygon clusters to the Ray cluster
        polygon_futures = [
            ray.put(polygon_cluster) for polygon_cluster in polygon_clusters
        ]

        # Apply NMS to each cluster
        nms_polygons_futures = [
            ray.remote(apply_distributed_polygon_nms).remote(
                polygon_future, nms_method, intersection_func, threshold, sigma
            )
            for polygon_future in polygon_futures
        ]
        nms_polygons = ray.get(nms_polygons_futures)

        # Shutdown Ray if it was initialized in this function
        if initialize_ray:
            ray.shutdown()  # Shutdown Ray
    elif distributed == "Dask":
        # Use an existing Dask client if provided, or create a new one
        client = kwargs.get("client", None)
        if client is None:
            from dask.distributed import Client

            initialize_dask = True
            client = Client()
        else:
            initialize_dask = False

        # Scatter the polygon clusters to the Dask cluster
        polygon_futures = client.scatter(polygon_clusters)

        # Apply NMS to each cluster and gather the results
        nms_polygon_futures = client.map(
            functools.partial(
                apply_distributed_polygon_nms,
                nms_method=nms_method,
                intersection_method=intersection_func,
                threshold=threshold,
                sigma=sigma,
            ),
            polygon_futures,
        )
        nms_polygons = client.gather(nms_polygon_futures)
        # Shutdown Dask if it was initialized in this function
        if initialize_dask:
            client.close()  # Shutdown Dask
    else:
        # Apply NMS to each cluster
        nms_polygons = [
            apply_distributed_polygon_nms(
                polygon_cluster, nms_method, intersection_func, threshold, sigma
            )
            for polygon_cluster in polygon_clusters
        ]

    # Combine the kept polygon indices from each cluster and
    # get the keep indices from each cluster
    keep_indices = []
    for cluster, cluster_nms in zip(clusters, nms_polygons):
        for idx in cluster_nms:
            keep_indices.append(cluster[idx])

    return keep_indices

polygone_nms.utils

bbox_to_polygon_array(coords)

Convert bbox [xmin, ymin, xmax, ymax] to polygon format [xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax]

Parameters:

Name Type Description Default
coords np.ndarray

bbox coordinates

required

Returns:

Type Description
np.ndarray

np.ndarray: polygon coordinates

Source code in polygone_nms/utils.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def bbox_to_polygon_array(coords: np.ndarray) -> np.ndarray:
    """
    Convert bbox [xmin, ymin, xmax, ymax] to polygon format
    [xmin, ymin, xmax, ymin, xmax, ymax, xmin, ymax]

    Args:
        coords (np.ndarray): bbox coordinates
    Returns:
        np.ndarray: polygon coordinates
    """
    return np.array(
        [
            coords[0],
            coords[1],
            coords[2],
            coords[1],
            coords[2],
            coords[3],
            coords[0],
            coords[3],
        ]
    )

build_rtree(polygons)

Build an R-tree index from a list of tuples having polygon, class and confidence.

The R-tree index is used to perform spatial queries on the input polygons. The input polygons are represented as Shapely Polygons.

The R-tree index is built using the rtree library. More information about the R-tree library can be found here: https://pypi.org/project/rtree/

Examples:

>>> from shapely.geometry import Polygon
>>> from polygone_nms.utils import build_rtree
>>> p1 = (Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), 0, 0.9)
>>> p2 = (Polygon([(2, 0), (3, 0), (3, 1), (2, 1)]), 1, 0.8)
>>> p3 = (Polygon([(1, 2), (2, 2), (2, 3), (1, 3)]), 1, 0.7)
>>> rtree_index = build_rtree([p1, p2, p3])
>>> query_bounds = (0.5, 0.5, 1.5, 1.5)
>>> list(rtree_index.intersection(query_bounds))
[0]
>>> query_bounds = (1.5, 1.5, 2.5, 2.5)
>>> list(rtree_index.intersection(query_bounds))
[2]

Parameters:

Name Type Description Default
polygons List[Tuple[Polygon, float, float]]

A list of tuples having polygon, class and confidence.

required

Returns:

Type Description
rtree.index.Index

rtree.index.Index: An R-tree index containing the input polygons.

Source code in polygone_nms/utils.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def build_rtree(polygons: List[Tuple[Polygon, float, float]]) -> rtree.index.Index:
    """
    Build an R-tree index from a list of tuples having polygon, class and confidence.

    The R-tree index is used to perform spatial queries on the input polygons.
    The input polygons are represented as Shapely Polygons.

    The R-tree index is built using the rtree library.
    More information about the R-tree library can be found here:
    https://pypi.org/project/rtree/

    Examples:
        >>> from shapely.geometry import Polygon
        >>> from polygone_nms.utils import build_rtree
        >>> p1 = (Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), 0, 0.9)
        >>> p2 = (Polygon([(2, 0), (3, 0), (3, 1), (2, 1)]), 1, 0.8)
        >>> p3 = (Polygon([(1, 2), (2, 2), (2, 3), (1, 3)]), 1, 0.7)
        >>> rtree_index = build_rtree([p1, p2, p3])
        >>> query_bounds = (0.5, 0.5, 1.5, 1.5)
        >>> list(rtree_index.intersection(query_bounds))
        [0]
        >>> query_bounds = (1.5, 1.5, 2.5, 2.5)
        >>> list(rtree_index.intersection(query_bounds))
        [2]

    Args:
        polygons (List[Tuple[Polygon, float, float]]):
            A list of tuples having polygon, class and confidence.

    Returns:
        rtree.index.Index: An R-tree index containing the input polygons.
    """
    # Create an R-tree index
    rtree_idx = index.Index()

    # Iterate over the polygons and add them to the R-tree index
    for idx, row in enumerate(polygons):
        # Retrieve polygon, class, and confidence
        polygon, cls, conf = row

        # Calculate the bounding box of the polygon
        bbox = polygon.bounds  # Returns (minx, miny, maxx, maxy)

        # Insert the bounding box into the R-tree
        rtree_idx.insert(
            idx, bbox, obj={"polygon": polygon, "class": cls, "confidence": conf}
        )

    return rtree_idx

create_polygon(coords, none_value=-1.0)

Create a Shapely Polygon from a numpy array row of coordinates. If the number of coordinates is odd, an error is raised. If any of the coordinates is of none_value, it is ignored.

Examples:

>>> import numpy as np
>>> from polygone_nms.utils import create_polygon
>>> coords = np.array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0])
>>> poly = create_polygon(coords)
>>> poly.is_valid
True
>>> poly.area
1.0
>>> coords = np.array([2.0, 0.0, 3.0, 0.0, 3.0, 1.0, 2.0, 1.0, 2.0, 0.0])
>>> poly = create_polygon(coords)
>>> poly.is_valid
True
>>> poly.area
1.0
>>> coords = np.array([1.0, 2.0, 2.0, 2.0, 2.0, 3.0, -1.0, -1.0])
>>> poly = create_polygon(coords)
>>> poly.is_valid
True
>>> poly.area
0.5
>>> coords = np.array([1.0, 2.0, 2.0, 2.0, 2.0, 3.0, -1.0, -1.0, 1.0, 2.0])
>>> poly = create_polygon(coords)
Traceback (most recent call last):
    ...
ValueError: The number of coordinates must be even.

Raises:

Type Description
ValueError

If the number of coordinates is odd.

Parameters:

Name Type Description Default
coords np.ndarray

A numpy array of coordinates for a shapely.geometry.Polygon.

required
none_value float

A value to be ignored. Defaults to -1.0.

-1.0

Returns:

Name Type Description
Polygon Polygon

A Shapely Polygon.

Source code in polygone_nms/utils.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def create_polygon(coords: np.ndarray, none_value: float = -1.0) -> Polygon:
    """
    Create a Shapely Polygon from a numpy array row of coordinates.
    If the number of coordinates is odd, an error is raised.
    If any of the coordinates is of `none_value`, it is ignored.

    Examples:
        >>> import numpy as np
        >>> from polygone_nms.utils import create_polygon
        >>> coords = np.array([0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0])
        >>> poly = create_polygon(coords)
        >>> poly.is_valid
        True
        >>> poly.area
        1.0
        >>> coords = np.array([2.0, 0.0, 3.0, 0.0, 3.0, 1.0, 2.0, 1.0, 2.0, 0.0])
        >>> poly = create_polygon(coords)
        >>> poly.is_valid
        True
        >>> poly.area
        1.0
        >>> coords = np.array([1.0, 2.0, 2.0, 2.0, 2.0, 3.0, -1.0, -1.0])
        >>> poly = create_polygon(coords)
        >>> poly.is_valid
        True
        >>> poly.area
        0.5
        >>> coords = np.array([1.0, 2.0, 2.0, 2.0, 2.0, 3.0, -1.0, -1.0, 1.0, 2.0])
        >>> poly = create_polygon(coords)
        Traceback (most recent call last):
            ...
        ValueError: The number of coordinates must be even.

    Raises:
        ValueError: If the number of coordinates is odd.

    Args:
        coords (np.ndarray):
            A numpy array of coordinates for a shapely.geometry.Polygon.
        none_value (float, optional): A value to be ignored. Defaults to -1.0.

    Returns:
        Polygon: A Shapely Polygon.
    """
    assert len(coords.shape) == 1, "The coordinates must be a 1D array."
    if coords.shape[0] % 2 != 0:
        raise ValueError("The number of coordinates must be even.")
    assert coords.shape[0] % 2 == 0, "The number of coordinates must be even."

    if coords.shape[0] == 4:
        coords = bbox_to_polygon_array(coords)

    points = [
        (coords[i], coords[i + 1])
        for i in range(0, len(coords), 2)
        if coords[i] != none_value and coords[i + 1] != none_value
    ]
    return Polygon(points)

dfs_iterative(node, visited, adj_list)

Perform a depth-first search on an adjacency list (graph) in a iterative manner.

Examples:

>>> from polygone_nms.utils import dfs
>>> adj_list = [[1], [0, 2, 3], [1], [1]]
>>> visited = [False] * len(adj_list)
>>> dfs(0, visited, adj_list)
[0, 1, 2, 3]

Parameters:

Name Type Description Default
node int

The starting node for the DFS traversal.

required
visited List[bool]

list of booleans indicating whether a node has been visited.

required
adj_list List[List[int]]

The adjacency list representing the graph.

required

Returns:

Type Description
List[int]

List[int]: A list of nodes in the connected component found by the DFS traversal.

Source code in polygone_nms/utils.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def dfs_iterative(
    node: int, visited: List[bool], adj_list: List[List[int]]
) -> List[int]:
    """
    Perform a depth-first search on an adjacency list (graph) in a iterative manner.

    Examples:
        >>> from polygone_nms.utils import dfs
        >>> adj_list = [[1], [0, 2, 3], [1], [1]]
        >>> visited = [False] * len(adj_list)
        >>> dfs(0, visited, adj_list)
        [0, 1, 2, 3]

    Args:
        node (int): The starting node for the DFS traversal.
        visited (List[bool]):
            list of booleans indicating whether a node has been visited.
        adj_list (List[List[int]]): The adjacency list representing the graph.

    Returns:
        List[int]:
            A list of nodes in the connected component found by the DFS traversal.
    """
    # Create a list to store the connected component
    connected_component = []

    # Create a stack
    stack = [node]

    # Iterate over the stack
    while stack:
        # Pop the last element from the stack
        curr_node = stack.pop()
        # If the node has not been visited, mark it as visited and
        # add it to the connected component
        if not visited[curr_node]:
            visited[curr_node] = True
            connected_component.append(curr_node)
            # Add the neighbors of the current node to the stack if
            # they have not been visited
            stack.extend(
                reversed(
                    [
                        neighbor
                        for neighbor in adj_list[curr_node]
                        if not visited[neighbor]
                    ]
                )
            )

    return connected_component

dfs_recursive(node, visited, adj_list)

Perform a depth-first search on an adjacency list (graph) in a recursive manner.

Examples:

>>> from polygone_nms.utils import dfs
>>> adj_list = [[1], [0, 2, 3], [1], [1]]
>>> visited = [False] * len(adj_list)
>>> dfs(0, visited, adj_list)
[0, 1, 2, 3]

Parameters:

Name Type Description Default
node int

The starting node for the DFS traversal.

required
visited List[bool]

list of booleans indicating whether a node has been visited.

required
adj_list List[List[int]]

The adjacency list representing the graph.

required

Returns:

Type Description
List[int]

List[int]: A list of nodes in the connected component found by the DFS traversal.

Source code in polygone_nms/utils.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def dfs_recursive(
    node: int, visited: List[bool], adj_list: List[List[int]]
) -> List[int]:
    """
    Perform a depth-first search on an adjacency list (graph) in a recursive manner.

    Examples:
        >>> from polygone_nms.utils import dfs
        >>> adj_list = [[1], [0, 2, 3], [1], [1]]
        >>> visited = [False] * len(adj_list)
        >>> dfs(0, visited, adj_list)
        [0, 1, 2, 3]

    Args:
        node (int): The starting node for the DFS traversal.
        visited (List[bool]):
            list of booleans indicating whether a node has been visited.
        adj_list (List[List[int]]): The adjacency list representing the graph.

    Returns:
        List[int]:
            A list of nodes in the connected component found by the DFS traversal.
    """
    # Mark the node as visited
    visited[node] = True

    # Add the node to the list
    connected_component = [node]

    # Iterate over the neighbors of the node
    for neighbor in adj_list[node]:
        # If the neighbor has not been visited, perform a depth-first search on it
        if not visited[neighbor]:
            # Extend the connected component with the connected component
            # found by the DFS traversal pf the neighbor
            connected_component.extend(dfs_recursive(neighbor, visited, adj_list))

    return connected_component

dice(poly1, poly2)

Compute the dice coefficient between two Shapely Polygons.

The Dice coefficient is a similarity measure used in image segmentation tasks, particularly for comparing binary segmentation masks. It is defined as the ratio of twice the area of intersection between the predicted and ground truth masks to the sum of their areas.

Dice=(2 * Area of Intersection) / (Area of First Polygon + Area of Second Polygon)

The Dice coefficient ranges from 0 (no overlap) to 1 (perfect overlap).

Notes

It is more sensitive to the size of the regions than IoU and is particularly useful for evaluating the performance of segmentation algorithms in cases where the regions of interest have varying sizes and shapes.

Examples:

>>> from shapely.geometry import Polygon
>>> from polygone_nms.utils import dice
>>> poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
>>> poly2 = Polygon([(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1)])
>>> dice(poly1, poly2)
0.5

Parameters:

Name Type Description Default
poly1 Polygon

The first polygon represented by a shapely.geometry.Polygon object.

required
poly2 Polygon

The second polygon represented by a shapely.geometry.Polygon object.

required

Returns:

Name Type Description
float float

The Dice value between the two Shapely Polygons.

Source code in polygone_nms/utils.py
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
def dice(poly1: Polygon, poly2: Polygon) -> float:
    """
    Compute the dice coefficient between two Shapely Polygons.

    The Dice coefficient is a similarity measure used in image segmentation tasks,
    particularly for comparing binary segmentation masks.
    It is defined as the ratio of twice the area of intersection
    between the predicted and ground truth masks to the sum of their areas.

    Dice=(2 * Area of Intersection) / (Area of First Polygon + Area of Second Polygon)

    The Dice coefficient ranges from 0 (no overlap) to 1 (perfect overlap).

    Notes:
        It is more sensitive to the size of the regions than IoU and is
        particularly useful for evaluating the performance of segmentation algorithms
        in cases where the regions of interest have varying sizes and shapes.

    Examples:
        >>> from shapely.geometry import Polygon
        >>> from polygone_nms.utils import dice
        >>> poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
        >>> poly2 = Polygon([(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1)])
        >>> dice(poly1, poly2)
        0.5

    Args:
        poly1 (Polygon):
            The first polygon represented by a shapely.geometry.Polygon object.
        poly2 (Polygon):
            The second polygon represented by a shapely.geometry.Polygon object.
    Returns:
        float: The Dice value between the two Shapely Polygons.
    """
    # Calculate the intersection
    intersection = poly1.intersection(poly2)

    # Calculate the sum area
    sum_area = poly1.area + poly2.area

    # If the sum area is 0, than Polygons are empty in future are information #TODO
    if sum_area == 0:
        return 0

    # Calculate the Dice coefficient
    return 2 * intersection.area / sum_area

ios(poly1, poly2)

Compute the intersection over smaller (IoS) between two Shapely Polygons.

IoS is another overlap metric that measures the ratio of the area of intersection between two regions to the area of the smaller region.

IoS = (Area of Intersection) / (Area of the Smaller Region)

An IoU score of 1 means that the predicted and ground truth regions perfectly overlap, while a score of 0 means that there's no overlap at all.

Notes

Unlike IoU, IoS is more sensitive to the size of the regions being compared. In certain scenarios, using IoS can help to better evaluate the quality of detections, especially when dealing with objects of varying sizes.

Examples:

>>> from shapely.geometry import Polygon
>>> from polygone_nms.utils import ios
>>> poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
>>> poly2 = Polygon([(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1)])
>>> ios(poly1, poly2)
0.5

Parameters:

Name Type Description Default
poly1 Polygon

The first polygon represented by a shapely.geometry.Polygon object.

required
poly2 Polygon

The second polygon represented by a shapely.geometry.Polygon object.

required

Returns:

Name Type Description
float float

The IoS value between the two Shapely Polygons.

Source code in polygone_nms/utils.py
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
def ios(poly1: Polygon, poly2: Polygon) -> float:
    """
    Compute the intersection over smaller (IoS) between two Shapely Polygons.

    IoS is another overlap metric that measures the ratio of the area of
    intersection between two regions to the area of the smaller region.

    IoS = (Area of Intersection) / (Area of the Smaller Region)

    An IoU score of 1 means that the predicted and ground truth regions
    perfectly overlap, while a score of 0 means that there's no overlap at all.

    Notes:
        Unlike IoU, IoS is more sensitive to the size of the regions being compared.
        In certain scenarios, using IoS can help to better evaluate the quality
        of detections, especially when dealing with objects of varying sizes.

    Examples:
        >>> from shapely.geometry import Polygon
        >>> from polygone_nms.utils import ios
        >>> poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
        >>> poly2 = Polygon([(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1)])
        >>> ios(poly1, poly2)
        0.5

    Args:
        poly1 (Polygon):
            The first polygon represented by a shapely.geometry.Polygon object.
        poly2 (Polygon):
            The second polygon represented by a shapely.geometry.Polygon object.
    Returns:
        float: The IoS value between the two Shapely Polygons.
    """
    # Calculate the intersection area
    intersection_area = poly1.intersection(poly2).area

    # Calculate the area of the smaller polygon
    smaller_area = min(poly1.area, poly2.area)

    # If the smaller area is 0, return 0
    if smaller_area == 0:
        return 0

    # Calculate the IoS
    return intersection_area / smaller_area

iot(target, compared)

Compute the intersection over target (IoT) between two Shapely Polygons.

IoT is another overlap metric that measures the ratio of the area of intersection between the Target and Compared regions to the area of the Target region.

IoT = (Area of Intersection) / (Area of the Target Region)

An IoT score of 1 means that the compared region perfectly overlaps the target region, while a score of 0 means that there's no overlap at all.

Notes

This is a testing metrics designed for NMS algorithm. My intuition is that if used with NMS algorithm it can result in a better performance.

the predicted and ground truth regions perfectly overlap, while a score of 0 means that there's no overlap at all.

Examples:

>>> from shapely.geometry import Polygon
>>> from polygone_nms.utils import iot
>>> poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
>>> poly2 = Polygon([(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1)])
>>> iot(poly1, poly2)
0.5

Parameters:

Name Type Description Default
target Polygon

The target polygon represented by a shapely.geometry.Polygon object.

required
compared Polygon

The second polygon represented by a shapely.geometry.Polygon object.

required

Returns:

Name Type Description
float float

The IOT value between target and compared Shapely Polygons.

Source code in polygone_nms/utils.py
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
def iot(target: Polygon, compared: Polygon) -> float:
    """
    Compute the intersection over target (IoT) between two Shapely Polygons.

    IoT is another overlap metric that measures the ratio of the area of
    intersection between the Target and Compared regions to the area of
    the Target region.

    IoT = (Area of Intersection) / (Area of the Target Region)

    An IoT score of 1 means that the compared region perfectly overlaps
    the target region, while a score of 0 means that there's no overlap at all.

    Notes:
        This is a testing metrics designed for NMS algorithm.
        My intuition is that if used
        with NMS algorithm it can result in a better performance.

    the predicted and ground truth regions perfectly overlap,
    while a score of 0 means that there's no overlap at all.

    Examples:
        >>> from shapely.geometry import Polygon
        >>> from polygone_nms.utils import iot
        >>> poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
        >>> poly2 = Polygon([(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1)])
        >>> iot(poly1, poly2)
        0.5

    Args:
        target (Polygon):
            The target polygon represented by a shapely.geometry.Polygon object.
        compared (Polygon):
            The second polygon represented by a shapely.geometry.Polygon object.

    Returns:
        float: The IOT value between target and compared Shapely Polygons.
    """
    # Calculate the intersection area
    intersection_area = target.intersection(compared).area

    # Calculate the area of the target polygon
    target_area = target.area

    # If the target area is 0, than Polygons are empty in future are information #TODO
    if target_area == 0:
        return 0

    # Calculate the IoT
    return intersection_area / target_area

iou(poly1, poly2)

Compute the intersection over union (IoU) between two Shapely Polygons.

IoU is a popular metric for evaluating the quality of object detections and segmentations. It measures the ratio of the area of overlap between two regions (e.g., predicted and ground truth bounding boxes) to the area of their union.

IOU = Area of intersection / Area of union ~= Area of intersection / (Area of poly1 + Area of poly2 - Area of intersection)

An IoU score of 1 means that the predicted and ground truth regions perfectly overlap, while a score of 0 means that there's no overlap at all.

Notes

In object detection and segmentation tasks, a higher IoU threshold indicates a stricter evaluation criterion.

Examples:

>>> from shapely.geometry import Polygon
>>> from polygone_nms.utils import iou
>>> poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
>>> poly2 = Polygon([(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1)])
>>> iou(poly1, poly2)
0.3333333333333333

Parameters:

Name Type Description Default
poly1 Polygon

The first polygon represented by a shapely.geometry.Polygon object.

required
poly2 Polygon

The second polygon represented by a shapely.geometry.Polygon object.

required

Returns:

Name Type Description
float float

The IoU value between the two Shapely Polygons.

Source code in polygone_nms/utils.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def iou(poly1: Polygon, poly2: Polygon) -> float:
    """
    Compute the intersection over union (IoU) between two Shapely Polygons.

    IoU is a popular metric for evaluating the quality of
    object detections and segmentations. It measures the ratio of the area of overlap
    between two regions (e.g., predicted and ground truth bounding boxes)
    to the area of their union.

    IOU = Area of intersection / Area of union
       ~= Area of intersection / (Area of poly1 + Area of poly2 - Area of intersection)

    An IoU score of 1 means that the predicted and ground truth regions
    perfectly overlap, while a score of 0 means that there's no overlap at all.

    Notes:
        In object detection and segmentation tasks,
        a higher IoU threshold indicates a stricter evaluation criterion.

    Examples:
        >>> from shapely.geometry import Polygon
        >>> from polygone_nms.utils import iou
        >>> poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
        >>> poly2 = Polygon([(0.5, 0), (1.5, 0), (1.5, 1), (0.5, 1)])
        >>> iou(poly1, poly2)
        0.3333333333333333

    Args:
        poly1 (Polygon):
            The first polygon represented by a shapely.geometry.Polygon object.
        poly2 (Polygon):
            The second polygon represented by a shapely.geometry.Polygon object.

    Returns:
        float: The IoU value between the two Shapely Polygons.
    """
    # Calculate the intersection area
    intersection_area = poly1.intersection(poly2).area

    # Calculate the union
    union_area = poly1.area + poly2.area - intersection_area
    # union_area = poly1.union(poly2).area tested and it's slower

    # If the union area is 0, return 0
    if union_area == 0:
        return 0

    # Calculate the IoU
    return intersection_area / union_area