From fb1dbfd08427dd3df0653fe3a420bb4aa7a0cb8c Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Mon, 17 Feb 2025 02:26:04 +0300 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E2=9C=A8=20Add=20xyxy=5Fxywh=20fun?= =?UTF-8?q?ction=20and=20from=5Ftransformers=20method=20for=20KeyPoints=20?= =?UTF-8?q?class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onuralp SEZER --- supervision/__init__.py | 2 + supervision/detection/utils.py | 37 ++++++++++++++ supervision/keypoint/core.py | 89 ++++++++++++++++++++++++++++++++++ 3 files changed, 128 insertions(+) diff --git a/supervision/__init__.py b/supervision/__init__.py index 2b2a0082..c1b9d2c6 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -78,6 +78,7 @@ from supervision.detection.utils import ( xcycwh_to_xyxy, xywh_to_xyxy, xyxy_to_polygons, + xyxy_xywh, ) from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import ( @@ -226,4 +227,5 @@ __all__ = [ "xcycwh_to_xyxy", "xywh_to_xyxy", "xyxy_to_polygons", + "xyxy_xywh", ] diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index 0d5ec475..dfc66bab 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -321,6 +321,43 @@ def xywh_to_xyxy(xywh: np.ndarray) -> np.ndarray: return xyxy +def xyxy_xywh(xyxy: np.ndarray) -> np.ndarray: + """ + Converts bounding box coordinates from `(x_min, y_min, x_max, y_max)` + format to `(x, y, width, height)` format. + + Args: + xyxy (np.ndarray): A numpy array of shape `(N, 4)` where each row + corresponds to a bounding box in the format `(x_min, y_min, x_max, + y_max)`. + + Returns: + np.ndarray: A numpy array of shape `(N, 4)` where each row corresponds + to a bounding box in the format `(x, y, width, height)`. + + Examples: + ```python + import numpy as np + import supervision as sv + + xyxy = np.array([ + [10, 20, 40, 60], + [15, 25, 50, 70] + ]) + + sv.xyxy_xywh(xyxy=xyxy) + # array([ + # [10, 20, 30, 40], + # [15, 25, 35, 45] + # ]) + ``` + """ + xywh = xyxy.copy() + xywh[:, 2] = xyxy[:, 2] - xyxy[:, 0] + xywh[:, 3] = xyxy[:, 3] - xyxy[:, 1] + return xywh + + def xcycwh_to_xyxy(xcycwh: np.ndarray) -> np.ndarray: """ Converts bounding box coordinates from `(center_x, center_y, width, height)` diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 04dde4e1..4766ae73 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -510,6 +510,95 @@ class KeyPoints: else: return cls.empty() + @classmethod + def from_transformers(cls, transfomers_results: Any) -> KeyPoints: + """ + Create a `sv.KeyPoints` object from the + [Transformers](https://github.com/huggingface/transformers) inference result. + + Args: + transfomers_results (Any): The output of a + Transformers model containing instances with prediction data. + + Returns: + A `sv.KeyPoints` object containing the keypoint coordinates, class IDs, + and class names, and confidences of each keypoint. + + Example: + ```python + import requests + import torch + from PIL import Image + from transformers import ( + AutoProcessor, + RTDetrForObjectDetection, + VitPoseForPoseEstimation, + ) + + import supervision as sv + + device = "cuda" if torch.cuda.is_available() else "cpu" + image = Image.open() + + person_image_processor = AutoProcessor.from_pretrained("PekingU/rtdetr_r50vd_coco_o365") + person_model = RTDetrForObjectDetection.from_pretrained("PekingU/rtdetr_r50vd_coco_o365", device_map=device) + + inputs = person_image_processor(images=image, return_tensors="pt").to(device) + + with torch.no_grad(): + outputs = person_model(**inputs) + + results = person_image_processor.post_process_object_detection( + outputs, target_sizes=torch.tensor([(image.height, image.width)]), threshold=0.3 + ) + result = results[0] # take first image results + detections = sv.Detections.from_transformers(result) + person_detections_xywh = sv.xyxy_xywh(detections[detections.class_id == 0].xyxy) + + image_processor = AutoProcessor.from_pretrained("usyd-community/vitpose-base-simple") + model = VitPoseForPoseEstimation.from_pretrained( + "usyd-community/vitpose-base-simple", device_map=device + ) + + inputs = image_processor(image, boxes=[person_detections_xywh], return_tensors="pt").to( + device + ) + + with torch.no_grad(): + outputs = model(**inputs) + + pose_results = image_processor.post_process_pose_estimation( + outputs, boxes=[person_detections_xywh] + )[0] + + keypoints = sv.KeyPoints.from_transformers(pose_results) + + + ``` + """ # noqa: E501 // docs + + if "keypoints" in transfomers_results[0]: + if transfomers_results[0]["keypoints"].cpu().numpy().size == 0: + return cls.empty() + + result_data = [ + ( + result["keypoints"].cpu().numpy(), + result["scores"].cpu().numpy(), + ) + for result in transfomers_results + ] + + xy, scores = zip(*result_data) + + return cls( + xy=np.stack(xy).astype(np.float32), + confidence=np.stack(scores).astype(np.float32), + class_id=np.arange(len(xy)).astype(int), + ) + else: + return cls.empty() + def __getitem__( self, index: Union[int, slice, List[int], np.ndarray, str] ) -> Union[KeyPoints, List, np.ndarray, None]: From cbaa605e08fc86d913b09dd0672cd913449bca91 Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Tue, 18 Feb 2025 00:52:27 +0300 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=F0=9F=94=84=20Rename=20xyxy=5F?= =?UTF-8?q?xywh=20function=20to=20xyxy=5Fto=5Fxywh=20docs:=20=F0=9F=93=9D?= =?UTF-8?q?=20Update=20docstring=20for=20KeyPoints.from=5Ftransformers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onuralp SEZER --- supervision/__init__.py | 4 +-- supervision/detection/utils.py | 4 +-- supervision/keypoint/core.py | 52 ++++++++++++++++------------------ test/detection/test_utils.py | 24 ++++++++++++++++ 4 files changed, 52 insertions(+), 32 deletions(-) diff --git a/supervision/__init__.py b/supervision/__init__.py index c1b9d2c6..60b48a3d 100644 --- a/supervision/__init__.py +++ b/supervision/__init__.py @@ -78,7 +78,7 @@ from supervision.detection.utils import ( xcycwh_to_xyxy, xywh_to_xyxy, xyxy_to_polygons, - xyxy_xywh, + xyxy_to_xywh, ) from supervision.draw.color import Color, ColorPalette from supervision.draw.utils import ( @@ -227,5 +227,5 @@ __all__ = [ "xcycwh_to_xyxy", "xywh_to_xyxy", "xyxy_to_polygons", - "xyxy_xywh", + "xyxy_to_xywh", ] diff --git a/supervision/detection/utils.py b/supervision/detection/utils.py index dfc66bab..61d0c0cb 100644 --- a/supervision/detection/utils.py +++ b/supervision/detection/utils.py @@ -321,7 +321,7 @@ def xywh_to_xyxy(xywh: np.ndarray) -> np.ndarray: return xyxy -def xyxy_xywh(xyxy: np.ndarray) -> np.ndarray: +def xyxy_to_xywh(xyxy: np.ndarray) -> np.ndarray: """ Converts bounding box coordinates from `(x_min, y_min, x_max, y_max)` format to `(x, y, width, height)` format. @@ -345,7 +345,7 @@ def xyxy_xywh(xyxy: np.ndarray) -> np.ndarray: [15, 25, 50, 70] ]) - sv.xyxy_xywh(xyxy=xyxy) + sv.xyxy_to_xywh(xyxy=xyxy) # array([ # [10, 20, 30, 40], # [15, 25, 35, 45] diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index 4766ae73..dca0334e 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -526,53 +526,49 @@ class KeyPoints: Example: ```python - import requests - import torch from PIL import Image + import requests + import supervision as sv + import torch from transformers import ( AutoProcessor, RTDetrForObjectDetection, VitPoseForPoseEstimation, ) - import supervision as sv - device = "cuda" if torch.cuda.is_available() else "cpu" image = Image.open() - person_image_processor = AutoProcessor.from_pretrained("PekingU/rtdetr_r50vd_coco_o365") - person_model = RTDetrForObjectDetection.from_pretrained("PekingU/rtdetr_r50vd_coco_o365", device_map=device) + DETECTION_MODEL_ID = "PekingU/rtdetr_r50vd_coco_o365" - inputs = person_image_processor(images=image, return_tensors="pt").to(device) + detection_processor = AutoProcessor.from_pretrained(DETECTION_MODEL_ID, use_fast=True) + detection_model = RTDetrForObjectDetection.from_pretrained(DETECTION_MODEL_ID, device_map=DEVICE) + + inputs = detection_processor(images=frame, return_tensors="pt").to(DEVICE) with torch.no_grad(): - outputs = person_model(**inputs) + outputs = detection_model(**inputs) - results = person_image_processor.post_process_object_detection( - outputs, target_sizes=torch.tensor([(image.height, image.width)]), threshold=0.3 - ) - result = results[0] # take first image results - detections = sv.Detections.from_transformers(result) - person_detections_xywh = sv.xyxy_xywh(detections[detections.class_id == 0].xyxy) + target_size = torch.tensor([(frame.height, frame.width)]) + results = detection_processor.post_process_object_detection( + outputs, target_sizes=target_size, threshold=0.3) - image_processor = AutoProcessor.from_pretrained("usyd-community/vitpose-base-simple") - model = VitPoseForPoseEstimation.from_pretrained( - "usyd-community/vitpose-base-simple", device_map=device - ) + detections = sv.Detections.from_transformers(results[0]) + boxes = sv.xyxy_to_xywh(detections[detections.class_id == 0].xyxy) - inputs = image_processor(image, boxes=[person_detections_xywh], return_tensors="pt").to( - device - ) + POSE_ESTIMATION_MODEL_ID = "usyd-community/vitpose-base-simple" + + pose_estimation_processor = AutoProcessor.from_pretrained(POSE_ESTIMATION_MODEL_ID) + pose_estimation_model = VitPoseForPoseEstimation.from_pretrained( + POSE_ESTIMATION_MODEL_ID, device_map=DEVICE) + + inputs = pose_estimation_processor(frame, boxes=[boxes], return_tensors="pt").to(DEVICE) with torch.no_grad(): - outputs = model(**inputs) - - pose_results = image_processor.post_process_pose_estimation( - outputs, boxes=[person_detections_xywh] - )[0] - - keypoints = sv.KeyPoints.from_transformers(pose_results) + outputs = pose_estimation_model(**inputs) + results = pose_estimation_processor.post_process_pose_estimation(outputs, boxes=[boxes]) + key_point = sv.KeyPoints.from_transformers(results[0]) ``` """ # noqa: E501 // docs diff --git a/test/detection/test_utils.py b/test/detection/test_utils.py index d93c72c8..ed48ec86 100644 --- a/test/detection/test_utils.py +++ b/test/detection/test_utils.py @@ -21,6 +21,7 @@ from supervision.detection.utils import ( scale_boxes, xcycwh_to_xyxy, xywh_to_xyxy, + xyxy_to_xywh, ) TEST_MASK = np.zeros((1, 1000, 1000), dtype=bool) @@ -1381,6 +1382,29 @@ def test_xywh_to_xyxy(xywh: np.ndarray, expected_result: np.ndarray) -> None: np.testing.assert_array_equal(result, expected_result) +@pytest.mark.parametrize( + "xyxy, expected_result", + [ + (np.array([[10, 20, 40, 60]]), np.array([[10, 20, 30, 40]])), # standard case + (np.array([[0, 0, 0, 0]]), np.array([[0, 0, 0, 0]])), # zero size bounding box + ( + np.array([[50, 50, 150, 150]]), + np.array([[50, 50, 100, 100]]), + ), # large bounding box + ( + np.array([[-10, -20, 20, 20]]), + np.array([[-10, -20, 30, 40]]), + ), # negative coordinates + (np.array([[50, 50, 50, 80]]), np.array([[50, 50, 0, 30]])), # zero width + (np.array([[50, 50, 70, 50]]), np.array([[50, 50, 20, 0]])), # zero height + (np.array([]).reshape(0, 4), np.array([]).reshape(0, 4)), # empty array + ], +) +def test_xyxy_to_xywh(xyxy: np.ndarray, expected_result: np.ndarray) -> None: + result = xyxy_to_xywh(xyxy) + np.testing.assert_array_equal(result, expected_result) + + @pytest.mark.parametrize( "xcycwh, expected_result", [ From a151685a57415f2bdc5f3ec07a43bf5e66ecca28 Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Tue, 18 Feb 2025 01:05:56 +0300 Subject: [PATCH 3/4] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20Add=20document?= =?UTF-8?q?ation=20for=20xyxy=5Fto=5Fxywh=20function=20in=20utils.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/detection/utils.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/detection/utils.md b/docs/detection/utils.md index 5f1902b7..25c1cc7d 100644 --- a/docs/detection/utils.md +++ b/docs/detection/utils.md @@ -89,6 +89,12 @@ status: new :::supervision.detection.utils.xywh_to_xyxy + + +:::supervision.detection.utils.xyxy_to_xywh + From c06d5f01315627b6b4e3238baf637779ec1d0767 Mon Sep 17 00:00:00 2001 From: Onuralp SEZER Date: Tue, 18 Feb 2025 01:38:11 +0300 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20=E2=9C=8F=EF=B8=8F=20Update=20docst?= =?UTF-8?q?rings=20to=20correct=20example=20wording=20and=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- supervision/keypoint/core.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/supervision/keypoint/core.py b/supervision/keypoint/core.py index dca0334e..a625527f 100644 --- a/supervision/keypoint/core.py +++ b/supervision/keypoint/core.py @@ -61,6 +61,7 @@ class KeyPoints: method, which accepts [MediaPipe](https://github.com/google-ai-edge/mediapipe) pose result. + ```python import cv2 import mediapipe as mp @@ -314,6 +315,7 @@ class KeyPoints: key_points = sv.KeyPoints.from_mediapipe( face_landmarker_result, (image_width, image_height)) ``` + """ # noqa: E501 // docs if hasattr(mediapipe_results, "pose_landmarks"): results = mediapipe_results.pose_landmarks @@ -473,7 +475,7 @@ class KeyPoints: A `sv.KeyPoints` object containing the keypoint coordinates, class IDs, and class names, and confidences of each keypoint. - Example: + Examples: ```python import cv2 import supervision as sv @@ -524,7 +526,7 @@ class KeyPoints: A `sv.KeyPoints` object containing the keypoint coordinates, class IDs, and class names, and confidences of each keypoint. - Example: + Examples: ```python from PIL import Image import requests @@ -569,8 +571,8 @@ class KeyPoints: results = pose_estimation_processor.post_process_pose_estimation(outputs, boxes=[boxes]) key_point = sv.KeyPoints.from_transformers(results[0]) - ``` + """ # noqa: E501 // docs if "keypoints" in transfomers_results[0]: @@ -724,7 +726,7 @@ class KeyPoints: Returns: detections (Detections): The converted detections object. - Example: + Examples: ```python keypoints = sv.KeyPoints.from_inference(...) detections = keypoints.as_detections()