diff --git a/README.md b/README.md
index a6688a957..8042fda01 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@
>
+
@@ -53,6 +54,10 @@
+
+
+[![Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-detect-and-count-objects-in-polygon-zone.ipynb)
+
## 👋 hello
diff --git a/supervision/__init__.py b/supervision/__init__.py
index ea2f155ea..1ae9bc686 100644
--- a/supervision/__init__.py
+++ b/supervision/__init__.py
@@ -1,8 +1,8 @@
-__version__ = "0.2.1"
+__version__ = "0.3.0"
from supervision.detection.core import BoxAnnotator, Detections
-from supervision.detection.polygon_zone import PolygonZone, PolygonZoneAnnotator
from supervision.detection.line_counter import LineZone, LineZoneAnnotator
+from supervision.detection.polygon_zone import PolygonZone, PolygonZoneAnnotator
from supervision.detection.utils import generate_2d_mask
from supervision.draw.color import Color, ColorPalette
from supervision.draw.utils import draw_filled_rectangle, draw_polygon, draw_text
diff --git a/supervision/detection/core.py b/supervision/detection/core.py
index cef6c577d..64f36907e 100644
--- a/supervision/detection/core.py
+++ b/supervision/detection/core.py
@@ -1,11 +1,12 @@
from __future__ import annotations
from dataclasses import dataclass
-from typing import List, Optional, Union
+from typing import Iterator, List, Optional, Tuple, Union
import cv2
import numpy as np
+from supervision.detection.utils import non_max_suppression
from supervision.draw.color import Color, ColorPalette
from supervision.geometry.core import Position
@@ -17,22 +18,26 @@ class Detections:
Attributes:
xyxy (np.ndarray): An array of shape `(n, 4)` containing the bounding boxes coordinates in format `[x1, y1, x2, y2]`
- confidence (np.ndarray): An array of shape `(n,)` containing the confidence scores of the detections.
+ confidence (Optional[np.ndarray]): An array of shape `(n,)` containing the confidence scores of the detections.
class_id (np.ndarray): An array of shape `(n,)` containing the class ids of the detections.
tracker_id (Optional[np.ndarray]): An array of shape `(n,)` containing the tracker ids of the detections.
"""
xyxy: np.ndarray
- confidence: np.ndarray
class_id: np.ndarray
+ confidence: Optional[np.ndarray] = None
tracker_id: Optional[np.ndarray] = None
def __post_init__(self):
n = len(self.xyxy)
validators = [
(isinstance(self.xyxy, np.ndarray) and self.xyxy.shape == (n, 4)),
- (isinstance(self.confidence, np.ndarray) and self.confidence.shape == (n,)),
(isinstance(self.class_id, np.ndarray) and self.class_id.shape == (n,)),
+ self.confidence is None
+ or (
+ isinstance(self.confidence, np.ndarray)
+ and self.confidence.shape == (n,)
+ ),
self.tracker_id is None
or (
isinstance(self.tracker_id, np.ndarray)
@@ -42,7 +47,7 @@ def __post_init__(self):
if not all(validators):
raise ValueError(
"xyxy must be 2d np.ndarray with (n, 4) shape, "
- "confidence must be 1d np.ndarray with (n,) shape, "
+ "confidence must be None or 1d np.ndarray with (n,) shape, "
"class_id must be 1d np.ndarray with (n,) shape, "
"tracker_id must be None or 1d np.ndarray with (n,) shape"
)
@@ -53,14 +58,16 @@ def __len__(self):
"""
return len(self.xyxy)
- def __iter__(self):
+ def __iter__(
+ self,
+ ) -> Iterator[Tuple[np.ndarray, Optional[float], int, Optional[Union[str, int]]]]:
"""
Iterates over the Detections object and yield a tuple of `(xyxy, confidence, class_id, tracker_id)` for each detection.
"""
for i in range(len(self.xyxy)):
yield (
self.xyxy[i],
- self.confidence[i],
+ self.confidence[i] if self.confidence is not None else None,
self.class_id[i],
self.tracker_id[i] if self.tracker_id is not None else None,
)
@@ -69,11 +76,17 @@ def __eq__(self, other: Detections):
return all(
[
np.array_equal(self.xyxy, other.xyxy),
- np.array_equal(self.confidence, other.confidence),
+ any(
+ [
+ self.confidence is None and other.confidence is None,
+ np.array_equal(self.confidence, other.confidence),
+ ]
+ ),
np.array_equal(self.class_id, other.class_id),
any(
[
self.tracker_id is None and other.tracker_id is None,
+ np.array_equal(self.tracker_id, other.tracker_id),
]
),
]
@@ -122,7 +135,7 @@ def from_yolov8(cls, yolov8_results):
>>> from supervision import Detections
>>> model = YOLO('yolov8s.pt')
- >>> results = model(frame)
+ >>> results = model(frame)[0]
>>> detections = Detections.from_yolov8(results)
```
"""
@@ -132,6 +145,36 @@ def from_yolov8(cls, yolov8_results):
class_id=yolov8_results.boxes.cls.cpu().numpy().astype(int),
)
+ @classmethod
+ def from_transformers(cls, transformers_results: dict):
+ return cls(
+ xyxy=transformers_results["boxes"].cpu().numpy(),
+ confidence=transformers_results["scores"].cpu().numpy(),
+ class_id=transformers_results["labels"].cpu().numpy().astype(int),
+ )
+
+ @classmethod
+ def from_detectron2(cls, detectron2_results):
+ return cls(
+ xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(),
+ confidence=detectron2_results["instances"].scores.cpu().numpy(),
+ class_id=detectron2_results["instances"]
+ .pred_classes.cpu()
+ .numpy()
+ .astype(int),
+ )
+
+ @classmethod
+ def from_coco_annotations(cls, coco_annotation: dict):
+ xyxy, class_id = [], []
+
+ for annotation in coco_annotation:
+ x_min, y_min, width, height = annotation["bbox"]
+ xyxy.append([x_min, y_min, x_min + width, y_min + height])
+ class_id.append(annotation["category_id"])
+
+ return cls(xyxy=np.array(xyxy), class_id=np.array(class_id))
+
def filter(self, mask: np.ndarray, inplace: bool = False) -> Optional[Detections]:
"""
Filter the detections by applying a mask.
@@ -186,7 +229,9 @@ def get_anchor_coordinates(self, anchor: Position) -> np.ndarray:
raise ValueError(f"{anchor} is not supported.")
def __getitem__(self, index: np.ndarray) -> Detections:
- if isinstance(index, np.ndarray) and index.dtype == bool:
+ if isinstance(index, np.ndarray) and (
+ index.dtype == bool or index.dtype == int
+ ):
return Detections(
xyxy=self.xyxy[index],
confidence=self.confidence[index],
@@ -199,6 +244,17 @@ def __getitem__(self, index: np.ndarray) -> Detections:
f"Detections.__getitem__ not supported for index of type {type(index)}."
)
+ @property
+ def area(self) -> np.ndarray:
+ return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0])
+
+ def with_nms(self, threshold: float = 0.5) -> Detections:
+ assert (
+ self.confidence is not None
+ ), f"Detections confidence must be given for NMS to be executed."
+ indices = non_max_suppression(self.xyxy, self.confidence, threshold=threshold)
+ return self[indices]
+
class BoxAnnotator:
def __init__(
@@ -266,7 +322,7 @@ def annotate(
continue
text = (
- f"{confidence:0.2f}"
+ f"{class_id}"
if (labels is None or len(detections) != len(labels))
else labels[i]
)
diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py
index a3a33490f..0bb2734d3 100644
--- a/supervision/detection/utils.py
+++ b/supervision/detection/utils.py
@@ -18,3 +18,40 @@ def generate_2d_mask(polygon: np.ndarray, resolution_wh: Tuple[int, int]) -> np.
mask = np.zeros((height, width), dtype=np.uint8)
cv2.fillPoly(mask, [polygon], color=1)
return mask
+
+
+def non_max_suppression(boxes: np.ndarray, scores: np.ndarray, threshold: float):
+ assert boxes.shape[0] == scores.shape[0]
+ ys1 = boxes[:, 0]
+ xs1 = boxes[:, 1]
+ ys2 = boxes[:, 2]
+ xs2 = boxes[:, 3]
+
+ areas = (ys2 - ys1) * (xs2 - xs1)
+ scores_indexes = scores.argsort().tolist()
+ boxes_keep_index = []
+ while len(scores_indexes):
+ index = scores_indexes.pop()
+ boxes_keep_index.append(index)
+ if not len(scores_indexes):
+ break
+ iou = compute_iou(
+ boxes[index], boxes[scores_indexes], areas[index], areas[scores_indexes]
+ )
+ filtered_indexes = set((iou > threshold).nonzero()[0])
+ scores_indexes = [
+ v for (i, v) in enumerate(scores_indexes) if i not in filtered_indexes
+ ]
+ return np.array(boxes_keep_index)
+
+
+def compute_iou(box, boxes, box_area, boxes_area):
+ assert boxes.shape[0] == boxes_area.shape[0]
+ ys1 = np.maximum(box[0], boxes[:, 0])
+ xs1 = np.maximum(box[1], boxes[:, 1])
+ ys2 = np.minimum(box[2], boxes[:, 2])
+ xs2 = np.minimum(box[3], boxes[:, 3])
+ intersections = np.maximum(ys2 - ys1, 0) * np.maximum(xs2 - xs1, 0)
+ unions = box_area + boxes_area - intersections
+ iou = intersections / unions
+ return iou