from __future__ import annotations

import warnings
from collections.abc import Iterator
from dataclasses import dataclass, field
from functools import reduce
from typing import Any, cast

import numpy as np
import numpy.typing as npt
from deprecate import deprecated, void

from supervision.config import (
    CLASS_NAME_DATA_FIELD,
    ORIENTED_BOX_COORDINATES,
)
from supervision.detection._geometry_dispatch import detection_area, detection_iou
from supervision.detection.compact_mask import CompactMask
from supervision.detection.tools.transformers import (
    process_transformers_detection_result,
    process_transformers_v4_segmentation_result,
    process_transformers_v5_segmentation_result,
)
from supervision.detection.utils._typing import (
    _DetectionDataType,
    _DetectionDataValueType,
    _MetadataType,
)
from supervision.detection.utils.boxes import (
    _oriented_box_anchors,
    xyxyxyxy_to_xyxy,
)
from supervision.detection.utils.converters import (
    mask_to_xyxy,
    polygon_to_mask,
    rle_to_mask,
    xywh_to_xyxy,
)
from supervision.detection.utils.internal import (
    extract_ultralytics_masks,
    get_data_item,
    is_data_equal,
    is_metadata_equal,
    merge_data,
    merge_metadata,
    process_roboflow_result,
)
from supervision.detection.utils.iou_and_nms import (
    OverlapMetric,
    box_non_max_merge,
    box_non_max_suppression,
    box_soft_non_max_suppression,
    mask_non_max_merge,
    mask_non_max_suppression,
    mask_soft_non_max_suppression,
    oriented_box_non_max_merge,
    oriented_box_non_max_suppression,
)
from supervision.detection.utils.masks import (
    calculate_masks_centroids,
)
from supervision.detection.vlm import (
    LMM,
    VLM,
    _validate_vlm_parameters,
    from_deepseek_vl_2,
    from_florence_2,
    from_google_gemini_2_0,
    from_google_gemini_2_5,
    from_google_gemini_3_5,
    from_moondream,
    from_paligemma,
    from_qwen_2_5_vl,
    from_qwen_3_vl,
)
from supervision.geometry.core import Position
from supervision.utils.internal import (
    SupervisionWarnings,
    get_instance_variables,
    warn_deprecated,
)
from supervision.validators import (
    _validate_data,
    _validate_detections_fields,
    _validate_resolution,
)


@dataclass
class Detections:
    """
    The `sv.Detections` class in the Supervision library standardizes results from
    various object detection and segmentation models into a consistent format. This
    class simplifies data manipulation and filtering, providing a uniform API for
    integration with Supervision [trackers](/trackers/), [annotators](/latest/detection/annotators/), and [tools](/detection/tools/line_zone/).

    === "Inference"

        Use [`sv.Detections.from_inference`](/detection/core/#supervision.detection.core.Detections.from_inference)
        method, which accepts model results from both detection and segmentation models.

        ```python
        from supervision import _cv2 as cv2
        import supervision as sv
        from inference import get_model

        model = get_model(model_id="yolov8n-640")
        image = cv2.imread("<SOURCE_IMAGE_PATH>")
        results = model.infer(image)[0]
        detections = sv.Detections.from_inference(results)
        ```

    === "Ultralytics"

        Use [`sv.Detections.from_ultralytics`](/detection/core/#supervision.detection.core.Detections.from_ultralytics)
        method, which accepts model results from both detection and segmentation models.

        ```python
        from supervision import _cv2 as cv2
        import supervision as sv
        from ultralytics import YOLO

        model = YOLO("yolov8n.pt")
        image = cv2.imread("<SOURCE_IMAGE_PATH>")
        results = model(image)[0]
        detections = sv.Detections.from_ultralytics(results)
        ```

    === "Transformers"

        Use [`sv.Detections.from_transformers`](/detection/core/#supervision.detection.core.Detections.from_transformers)
        method, which accepts model results from both detection and segmentation models.

        ```python
        import torch
        import supervision as sv
        from PIL import Image
        from transformers import DetrImageProcessor, DetrForObjectDetection

        processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
        model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")

        image = Image.open("<SOURCE_IMAGE_PATH>")
        inputs = processor(images=image, return_tensors="pt")

        with torch.no_grad():
            outputs = model(**inputs)

        width, height = image.size
        target_size = torch.tensor([[height, width]])
        results = processor.post_process_object_detection(
            outputs=outputs, target_sizes=target_size)[0]
        detections = sv.Detections.from_transformers(
            transformers_results=results,
            id2label=model.config.id2label)
        ```

    Attributes:
        xyxy: An array of shape `(n, 4)` containing
            the bounding boxes coordinates in format `[x1, y1, x2, y2]`
        mask: An array of shape `(n, H, W)` containing the segmentation masks
            (`bool` data type), or `None` when masks are not available, or as
            :class:`~supervision.detection.compact_mask.CompactMask`.
        confidence: An array of shape `(n,)` containing the confidence scores
            of the detections, or `None` when confidence values are not available.
        class_id: An array of shape `(n,)` containing the class ids of the
            detections, or `None` when class ids are not available.
        tracker_id: An array of shape `(n,)` containing the tracker ids of the
            detections, or `None` when tracker ids are not available.
        data: A dictionary containing additional
            data where each key is a string representing the data type, and the value
            is either a NumPy array or a list of corresponding data.
        metadata: A dictionary containing collection-level metadata
            that applies to the entire set of detections. This may include information such
            as the video name, camera parameters, timestamp, or other global metadata.
    """  # noqa: E501 // docs

    xyxy: npt.NDArray[np.number]
    mask: npt.NDArray[np.bool_] | CompactMask | None = None
    confidence: npt.NDArray[np.floating] | None = None
    class_id: npt.NDArray[np.integer] | None = None
    tracker_id: npt.NDArray[np.integer] | None = None
    data: _DetectionDataType = field(default_factory=dict)
    metadata: _MetadataType = field(default_factory=dict)

    def __post_init__(self) -> None:
        _validate_detections_fields(
            xyxy=self.xyxy,
            mask=self.mask,
            confidence=self.confidence,
            class_id=self.class_id,
            tracker_id=self.tracker_id,
            data=self.data,
        )

    def __len__(self) -> int:
        """
        Returns the number of detections in the Detections object.
        """
        return len(self.xyxy)

    def __iter__(
        self,
    ) -> Iterator[
        tuple[
            npt.NDArray[np.number],
            npt.NDArray[np.bool_] | None,
            np.generic | None,
            np.generic | None,
            np.generic | None,
            _DetectionDataType,
        ]
    ]:
        """
        Iterates over the Detections object and yield a tuple of
        `(xyxy, mask, confidence, class_id, tracker_id, data)` for each detection.
        """
        for i in range(len(self.xyxy)):
            yield (
                self.xyxy[i],
                self.mask[i] if self.mask is not None else None,
                self.confidence[i] if self.confidence is not None else None,
                self.class_id[i] if self.class_id is not None else None,
                self.tracker_id[i] if self.tracker_id is not None else None,
                get_data_item(self.data, i),
            )

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Detections):
            return NotImplemented

        def array_equal_or_none(
            a: npt.NDArray[np.generic] | None,
            b: npt.NDArray[np.generic] | None,
        ) -> bool:
            if a is None or b is None:
                return a is b
            return bool(np.array_equal(a, b))

        def mask_equal(
            a: npt.NDArray[np.generic] | CompactMask | None,
            b: npt.NDArray[np.generic] | CompactMask | None,
        ) -> bool:
            if a is None or b is None:
                return a is b
            if isinstance(a, CompactMask):
                return bool(a == b)
            if isinstance(b, CompactMask):
                return bool(b == a)
            return bool(np.array_equal(a, b))

        return all(
            [
                np.array_equal(self.xyxy, other.xyxy),
                mask_equal(self.mask, other.mask),
                array_equal_or_none(self.class_id, other.class_id),
                array_equal_or_none(self.confidence, other.confidence),
                array_equal_or_none(self.tracker_id, other.tracker_id),
                is_data_equal(self.data, other.data),
                is_metadata_equal(self.metadata, other.metadata),
            ]
        )

    @classmethod
    def from_yolov5(cls, yolov5_results: Any) -> Detections:
        """
        Creates a Detections instance from a
        [YOLOv5](https://github.com/ultralytics/yolov5) inference result.

        Args:
            yolov5_results: The output Detections instance from YOLOv5.

        Returns:
            A new Detections object.

        Example:
            ```python
            from supervision import _cv2 as cv2
            import torch
            import supervision as sv

            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            model = torch.hub.load('ultralytics/yolov5', 'yolov5s')
            result = model(image)
            detections = sv.Detections.from_yolov5(result)
            ```
        """
        yolov5_detections_predictions = yolov5_results.pred[0].cpu().cpu().numpy()

        return cls(
            xyxy=yolov5_detections_predictions[:, :4],
            confidence=yolov5_detections_predictions[:, 4],
            class_id=yolov5_detections_predictions[:, 5].astype(int),
        )

    @classmethod
    def from_ultralytics(cls, ultralytics_results: Any) -> Detections:
        """
        Creates a `sv.Detections` instance from a
        [YOLOv8](https://github.com/ultralytics/ultralytics) inference result.

        !!! Note

            `from_ultralytics` is compatible with
            [detection](https://docs.ultralytics.com/tasks/detect/),
            [segmentation](https://docs.ultralytics.com/tasks/segment/), and
            [OBB](https://docs.ultralytics.com/tasks/obb/) models.

        Args:
            ultralytics_results: The output Results instance from Ultralytics.

        Returns:
            A new Detections object.

        Example:
            ```python
            from supervision import _cv2 as cv2
            import supervision as sv
            from ultralytics import YOLO

            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            model = YOLO('yolov8s.pt')
            results = model(image)[0]
            detections = sv.Detections.from_ultralytics(results)
            ```
        """

        if hasattr(ultralytics_results, "obb") and ultralytics_results.obb is not None:
            class_id = ultralytics_results.obb.cls.cpu().numpy().astype(int)
            class_names = np.array([ultralytics_results.names[i] for i in class_id])
            oriented_box_coordinates = ultralytics_results.obb.xyxyxyxy.cpu().numpy()
            return cls(
                xyxy=ultralytics_results.obb.xyxy.cpu().numpy(),
                confidence=ultralytics_results.obb.conf.cpu().numpy(),
                class_id=class_id,
                tracker_id=(
                    ultralytics_results.obb.id.int().cpu().numpy()
                    if ultralytics_results.obb.id is not None
                    else None
                ),
                data={
                    ORIENTED_BOX_COORDINATES: oriented_box_coordinates,
                    CLASS_NAME_DATA_FIELD: class_names,
                },
            )

        if hasattr(ultralytics_results, "boxes") and ultralytics_results.boxes is None:
            masks = extract_ultralytics_masks(ultralytics_results)
            if masks is None:
                empty = cls.empty()
                empty.data = {CLASS_NAME_DATA_FIELD: np.empty(0, dtype=str)}
                return empty
            return cls(
                xyxy=mask_to_xyxy(masks),
                mask=masks,
                class_id=np.arange(len(ultralytics_results)),
            )

        if (
            hasattr(ultralytics_results, "boxes")
            and ultralytics_results.boxes is not None
        ):
            class_id = ultralytics_results.boxes.cls.cpu().numpy().astype(int)
            class_names = np.array([ultralytics_results.names[i] for i in class_id])
            return cls(
                xyxy=ultralytics_results.boxes.xyxy.cpu().numpy(),
                confidence=ultralytics_results.boxes.conf.cpu().numpy(),
                class_id=class_id,
                mask=extract_ultralytics_masks(ultralytics_results),
                tracker_id=(
                    ultralytics_results.boxes.id.int().cpu().numpy()
                    if ultralytics_results.boxes.id is not None
                    else None
                ),
                data={CLASS_NAME_DATA_FIELD: class_names},
            )

        empty = cls.empty()
        empty.data = {CLASS_NAME_DATA_FIELD: np.empty(0, dtype=str)}
        return empty

    @classmethod
    def from_yolo_nas(cls, yolo_nas_results: Any) -> Detections:
        """
        Creates a Detections instance from a
        [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md)
        inference result.

        Args:
            yolo_nas_results: The output Results instance from YOLO-NAS.
                ImageDetectionPrediction is coming from
                'super_gradients.training.models.prediction_results'.

        Returns:
            A new Detections object.

        Example:
            ```python
            from supervision import _cv2 as cv2
            from super_gradients.training import models
            import supervision as sv

            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            model = models.get('yolo_nas_l', pretrained_weights="coco")

            result = list(model.predict(image, conf=0.35))[0]
            detections = sv.Detections.from_yolo_nas(result)
            ```
        """
        if np.asarray(yolo_nas_results.prediction.bboxes_xyxy).shape[0] == 0:
            return cls.empty()

        return cls(
            xyxy=yolo_nas_results.prediction.bboxes_xyxy,
            confidence=yolo_nas_results.prediction.confidence,
            class_id=yolo_nas_results.prediction.labels.astype(int),
        )

    @classmethod
    def from_tensorflow(
        cls, tensorflow_results: dict[str, Any], resolution_wh: tuple[int, int]
    ) -> Detections:
        """
        Creates a Detections instance from a
        [Tensorflow Hub](https://www.tensorflow.org/hub/tutorials/tf2_object_detection)
        inference result.

        Args:
            tensorflow_results: Raw output dict from a TensorFlow Hub
                object-detection model. Must contain:
                ``"detection_boxes"`` (shape ``[1, N, 4]``, normalized
                ``[ymin, xmin, ymax, xmax]``), ``"detection_scores"``
                (shape ``[1, N]``), and ``"detection_classes"``
                (shape ``[1, N]``).
            resolution_wh: The input image resolution as `(width, height)`.
                Bounding boxes from Tensorflow are normalized and are scaled
                to absolute coordinates using this resolution.

        Returns:
            A new Detections object.

        Note:
            TensorFlow Hub object-detection models return bounding boxes
            normalized as ``[ymin, xmin, ymax, xmax]``. This method rescales
            them to absolute pixel coordinates and reorders them to ``xyxy``
            (``[xmin, ymin, xmax, ymax]``) before constructing the
            :class:`Detections` object.

        Example:
            ```python
            import tensorflow as tf
            import tensorflow_hub as hub
            import numpy as np
            from supervision import _cv2 as cv2

            module_handle = "https://tfhub.dev/tensorflow/centernet/hourglass_512x512_kpts/1"
            model = hub.load(module_handle)
            img = np.array(cv2.imread("<SOURCE_IMAGE_PATH>"))
            result = model(img)
            detections = sv.Detections.from_tensorflow(
                result, resolution_wh=(img.shape[1], img.shape[0])
            )
            ```
        """

        # Tensorflow returns normalized boxes as [ymin, xmin, ymax, xmax], so the
        # y coordinates (cols 0, 2) scale by height and x (cols 1, 3) by width.
        # `.numpy()` may share memory with the source tensor, so copy before the
        # in-place scaling to avoid mutating the caller's result / double-scaling.
        boxes = tensorflow_results["detection_boxes"][0].numpy().copy()
        boxes[:, [0, 2]] *= resolution_wh[1]
        boxes[:, [1, 3]] *= resolution_wh[0]
        boxes = boxes[:, [1, 0, 3, 2]]
        return cls(
            xyxy=boxes,
            confidence=tensorflow_results["detection_scores"][0].numpy(),
            class_id=tensorflow_results["detection_classes"][0].numpy().astype(int),
        )

    @classmethod
    def from_deepsparse(cls, deepsparse_results: Any) -> Detections:
        """
        Creates a Detections instance from a
        [DeepSparse](https://github.com/neuralmagic/deepsparse)
        inference result.

        Args:
            deepsparse_results: The output Results instance from DeepSparse.

        Returns:
            A new Detections object.

        Example:
            ```python
            import supervision as sv
            from deepsparse import Pipeline

            yolo_pipeline = Pipeline.create(
                task="yolo",
                model_path = "zoo:cv/detection/yolov5-l/pytorch/ultralytics/coco/pruned80_quant-none"
             )
            result = yolo_pipeline(<SOURCE IMAGE PATH>)
            detections = sv.Detections.from_deepsparse(result)
            ```
        """  # noqa: E501 // docs

        if np.asarray(deepsparse_results.boxes[0]).shape[0] == 0:
            return cls.empty()

        return cls(
            xyxy=np.array(deepsparse_results.boxes[0]),
            confidence=np.array(deepsparse_results.scores[0]),
            class_id=np.array(deepsparse_results.labels[0]).astype(float).astype(int),
        )

    @classmethod
    def from_mmdetection(cls, mmdet_results: Any) -> Detections:
        """
        Creates a Detections instance from a
        [mmdetection](https://github.com/open-mmlab/mmdetection) and
        [mmyolo](https://github.com/open-mmlab/mmyolo) inference result.

        Args:
            mmdet_results: The output Results instance from MMDetection.

        Returns:
            A new Detections object.

        Example:
            ```python
            from supervision import _cv2 as cv2
            import supervision as sv
            from mmdet.apis import init_detector, inference_detector

            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            model = init_detector("<CONFIG_PATH>", "<WEIGHTS_PATH>", device="<DEVICE>")

            result = inference_detector(model, image)
            detections = sv.Detections.from_mmdetection(result)
            ```
        """

        return cls(
            xyxy=mmdet_results.pred_instances.bboxes.cpu().numpy(),
            confidence=mmdet_results.pred_instances.scores.cpu().numpy(),
            class_id=mmdet_results.pred_instances.labels.cpu().numpy().astype(int),
            mask=(
                mmdet_results.pred_instances.masks.cpu().numpy()
                if "masks" in mmdet_results.pred_instances
                else None
            ),
        )

    @classmethod
    def from_transformers(
        cls,
        transformers_results: dict[str, Any],
        id2label: dict[int, str] | None = None,
    ) -> Detections:
        """
        Creates a Detections instance from object detection or panoptic, semantic
        and instance segmentation
        [Transformer](https://github.com/huggingface/transformers) inference result.

        Args:
            transformers_results: Inference results from your Transformers model.
                This can be either a dictionary containing valuable outputs like
                `scores`, `labels`, `boxes`, `masks`, `segments_info`, and
                `segmentation`, or a `torch.Tensor` holding a segmentation map
                where values represent class IDs.
            id2label: A dictionary mapping class IDs to labels, typically part of
                the `transformers` model configuration. If provided, the resulting
                dictionary will include class names.

        Returns:
            A new Detections object.

        Example:
            ```python
            import torch
            import supervision as sv
            from PIL import Image
            from transformers import DetrImageProcessor, DetrForObjectDetection

            processor = DetrImageProcessor.from_pretrained("facebook/detr-resnet-50")
            model = DetrForObjectDetection.from_pretrained("facebook/detr-resnet-50")

            image = Image.open("<SOURCE_IMAGE_PATH>")
            inputs = processor(images=image, return_tensors="pt")

            with torch.no_grad():
                outputs = model(**inputs)

            width, height = image.size
            target_size = torch.tensor([[height, width]])
            results = processor.post_process_object_detection(
                outputs=outputs, target_sizes=target_size)[0]

            detections = sv.Detections.from_transformers(
                transformers_results=results,
                id2label=model.config.id2label
            )
            ```
        """

        if (
            transformers_results.__class__.__name__ == "Tensor"
            or "segmentation" in transformers_results
        ):
            return cls(
                **process_transformers_v5_segmentation_result(
                    transformers_results, id2label
                )
            )

        if "masks" in transformers_results or "png_string" in transformers_results:
            return cls(
                **process_transformers_v4_segmentation_result(
                    transformers_results, id2label
                )
            )

        if "boxes" in transformers_results:
            return cls(
                **process_transformers_detection_result(transformers_results, id2label)
            )

        else:
            raise ValueError(
                "The provided Transformers results do not contain any valid fields."
                " Expected fields are 'boxes', 'masks', 'segments_info' or"
                " 'segmentation'."
            )

    @classmethod
    def from_detectron2(cls, detectron2_results: Any) -> Detections:
        """
        Create a Detections object from the
        [Detectron2](https://github.com/facebookresearch/detectron2) inference result.

        Args:
            detectron2_results: The output of a
                Detectron2 model containing instances with prediction data.

        Returns:
            A Detections object containing the bounding boxes,
                class IDs, and confidences of the predictions.

        Example:
            ```python
            from supervision import _cv2 as cv2
            import supervision as sv
            from detectron2.engine import DefaultPredictor
            from detectron2.config import get_cfg


            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            cfg = get_cfg()
            cfg.merge_from_file("<CONFIG_PATH>")
            cfg.MODEL.WEIGHTS = "<WEIGHTS_PATH>"
            predictor = DefaultPredictor(cfg)

            result = predictor(image)
            detections = sv.Detections.from_detectron2(result)
            ```
        """

        return cls(
            xyxy=detectron2_results["instances"].pred_boxes.tensor.cpu().numpy(),
            confidence=detectron2_results["instances"].scores.cpu().numpy(),
            mask=(
                detectron2_results["instances"].pred_masks.cpu().numpy()
                if hasattr(detectron2_results["instances"], "pred_masks")
                else None
            ),
            class_id=detectron2_results["instances"]
            .pred_classes.cpu()
            .numpy()
            .astype(int),
        )

    @classmethod
    def from_inference(
        cls,
        roboflow_result: dict[str, Any] | Any,
        *,
        compact_masks: bool = False,
    ) -> Detections:
        """
        Create a `sv.Detections` object from the [Roboflow](https://roboflow.com/)
        API inference result or the [Inference](https://inference.roboflow.com/)
        package results. This method extracts bounding boxes, class IDs,
        confidences, and class names from the Roboflow API result and encapsulates
        them into a Detections object.

        Args:
            roboflow_result: The result from the
                Roboflow API or Inference package containing predictions.
            compact_masks: When `True`, return segmentation masks as
                :class:`~supervision.detection.compact_mask.CompactMask`.
                The default `False` preserves the existing dense NumPy mask
                representation.

                Warning:
                    When `compact_masks=True`, the crop policy depends on how
                    each prediction encodes its mask:

                    - Native size-matched COCO-RLE (the RLE `size` equals the
                      image size) is **cropped to the detector bounding box**
                      (`xyxy`). For instance-segmentation models the detector
                      box may not tightly bound the mask, so pixels beyond the
                      box boundary are silently dropped.
                    - Polygon-derived masks (`points`) and size-mismatched
                      COCO-RLE masks (decoded, then resized to the image) are
                      retained **full-frame** and lose no pixels.

                    Because only the box-cropped path is lossy,
                    `from_inference(r)` and
                    `from_inference(r, compact_masks=True)` can return masks
                    with different areas and IoU **only** for native
                    size-matched COCO-RLE predictions. Use `compact_masks=True`
                    only when the memory savings outweigh the boundary loss on
                    that path.

        Returns:
            A Detections object containing the bounding boxes, class IDs,
                and confidences of the predictions.
                `detections.data["class_name"]` is always present as a
                string-dtype NumPy array aligned with the detections; it is
                empty (shape `(0,)`, dtype str) when `predictions` is empty
                or absent. `detections.tracker_id` is `None` when no
                predictions carry a tracker ID, or when only a subset do
                (mixed batch) — in that case all tracker IDs are dropped to
                preserve alignment with the bounding boxes. Similarly,
                `detections.mask` is `None` when no predictions include mask
                data, or when only a subset carry masks — all masks are dropped
                to preserve xyxy alignment.
                When `compact_masks=True` and all predictions carry mask data,
                `detections.mask` is a
                :class:`~supervision.detection.compact_mask.CompactMask` rather
                than a dense boolean array.

        Example:
            ```python
            from supervision import _cv2 as cv2
            import supervision as sv
            from inference import get_model

            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            model = get_model(model_id="yolov8s-640")

            result = model.infer(image)[0]
            detections = sv.Detections.from_inference(result)
            compact_detections = sv.Detections.from_inference(
                result, compact_masks=True
            )
            ```
        """
        if hasattr(roboflow_result, "dict"):
            roboflow_result = roboflow_result.dict(exclude_none=True, by_alias=True)
        elif hasattr(roboflow_result, "json"):
            roboflow_result = roboflow_result.json()
        masks: npt.NDArray[np.bool_] | CompactMask | None
        # Design note (ADR): the `compact_masks` flag changes the runtime type of
        # `detections.mask` from `NDArray[bool_]` to `CompactMask`, so every mask
        # consumer must branch on `isinstance(detections.mask, CompactMask)`. A
        # typed factory / `mask_format=` enum would be cleaner but would require a
        # deprecation cycle if introduced later.
        if compact_masks:
            xyxy, confidence, class_id, masks, trackers, data = process_roboflow_result(
                roboflow_result=roboflow_result, compact_masks=True
            )
        else:
            xyxy, confidence, class_id, masks, trackers, data = process_roboflow_result(
                roboflow_result=roboflow_result
            )

        if np.asarray(xyxy).shape[0] == 0:
            empty_detection = cls.empty()
            empty_detection.data = data
            return empty_detection

        return cls(
            xyxy=xyxy,
            confidence=confidence,
            class_id=class_id,
            mask=masks,
            tracker_id=trackers,
            data=data,
        )

    @classmethod
    def from_sam(cls, sam_result: list[dict[str, Any]]) -> Detections:
        """
        Creates a Detections instance from
        [Segment Anything Model](https://github.com/facebookresearch/segment-anything)
        inference result.

        Args:
            sam_result: The output Results instance from SAM.

        Returns:
            A new Detections object.

        Example:
            ```python
            import supervision as sv
            from segment_anything import (
                sam_model_registry,
                SamAutomaticMaskGenerator
             )

            sam_model_reg = sam_model_registry[MODEL_TYPE]
            sam = sam_model_reg(checkpoint=CHECKPOINT_PATH).to(device=DEVICE)
            mask_generator = SamAutomaticMaskGenerator(sam)
            sam_result = mask_generator.generate(IMAGE)
            detections = sv.Detections.from_sam(sam_result=sam_result)
            ```
        """

        sorted_generated_masks = sorted(
            sam_result, key=lambda x: x["area"], reverse=True
        )
        if len(sorted_generated_masks) == 0:
            return cls.empty()

        xywh = np.array([mask["bbox"] for mask in sorted_generated_masks])
        segmentations = [mask["segmentation"] for mask in sorted_generated_masks]
        first_segmentation = segmentations[0]

        if all(isinstance(segmentation, np.ndarray) for segmentation in segmentations):
            mask = np.stack(segmentations, axis=0)
        elif all(isinstance(segmentation, dict) for segmentation in segmentations):
            image_height, image_width = cast(
                tuple[int, int], tuple(int(v) for v in first_segmentation["size"])
            )
            mask = np.stack(
                [
                    rle_to_mask(
                        segmentation["counts"],
                        (image_width, image_height),
                    )
                    for segmentation in segmentations
                ],
                axis=0,
            )
        else:
            raise ValueError(
                "SAM segmentations must all be dense arrays or COCO RLE dictionaries."
            )

        xyxy = xywh_to_xyxy(xywh=xywh)
        return cls(xyxy=xyxy, mask=mask)

    @classmethod
    def from_sam3(
        cls, sam3_result: dict[str, Any] | Any, resolution_wh: tuple[int, int]
    ) -> Detections:
        """
        Creates a Detections instance from
        [SAM 3](https://github.com/facebookresearch/sam3) inference result.
        Supports both PVS and PCS SAM3 segmentation formats.

        Args:
            sam3_result: The output result from SAM 3 inference, either
                Sam3PromptResult from inference package or dict containing
                prompt_results with polygon predictions.
            resolution_wh: The width and height of the image used for mask
                generation.

        Returns:
            A new Detections object. The `class_id` field contains the prompt
                index for each polygon.

        Example:
            ```python
            from supervision import _cv2 as cv2
            import supervision as sv
            from inference.models.sam3 import SegmentAnything3
            from inference.core.entities.requests.sam3 import Sam3Prompt

            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            model = SegmentAnything3(
                model_id="sam3/sam3_final",
                api_key="<ROBOFLOW_API_KEY>"
            )

            prompts = [
                Sam3Prompt(type="text", text="car"),
                Sam3Prompt(type="text", text="tire"),
            ]

            result = model.segment_image(
                image=image,
                prompts=prompts,
                output_prob_thresh=0.5,
                format="polygon"
            )

            height, width = image.shape[:2]
            detections = sv.Detections.from_sam3(
                sam3_result=result,
                resolution_wh=(width, height)
            )
            ```
        """
        width, height = _validate_resolution(resolution_wh)

        masks = []
        confidences = []
        class_ids = []

        if isinstance(sam3_result, dict):
            prompt_results = sam3_result.get("prompt_results", [])
            if not prompt_results and "predictions" in sam3_result:
                prompt_results = [
                    {"predictions": sam3_result["predictions"], "prompt_index": 0}
                ]
        else:
            prompt_results = getattr(sam3_result, "prompt_results", [])
            if not prompt_results and hasattr(sam3_result, "predictions"):
                prompt_results = [
                    {
                        "predictions": getattr(sam3_result, "predictions"),
                        "prompt_index": 0,
                    }
                ]

        for i, prompt_result in enumerate(prompt_results):
            if isinstance(prompt_result, dict):
                predictions = prompt_result.get("predictions", [])
                prompt_index = prompt_result.get("prompt_index", i)
            else:
                predictions = getattr(prompt_result, "predictions", [])
                prompt_index = getattr(prompt_result, "prompt_index", i)

            for prediction in predictions:
                if isinstance(prediction, dict):
                    prediction_format = prediction.get("format")
                    if prediction_format and prediction_format != "polygon":
                        continue
                    pred_masks = prediction.get("masks", [])
                    confidence = prediction.get("confidence", 1.0)
                else:
                    prediction_format = getattr(prediction, "format", None)
                    if prediction_format and prediction_format != "polygon":
                        continue
                    pred_masks = getattr(prediction, "masks", [])
                    confidence = getattr(prediction, "confidence", 1.0)

                if not pred_masks:
                    continue

                full_mask: npt.NDArray[np.bool_] = np.zeros((height, width), dtype=bool)
                for poly in pred_masks:
                    polygon = np.array(poly, dtype=np.int32)
                    mask = polygon_to_mask(
                        polygon=polygon, resolution_wh=(width, height)
                    )
                    mask = mask.astype(bool, copy=False)
                    np.logical_or(full_mask, mask, out=full_mask)

                masks.append(full_mask)
                confidences.append(confidence)
                class_ids.append(prompt_index)

        if not masks:
            return cls.empty()

        masks_np = np.stack(masks, axis=0)
        xyxy = mask_to_xyxy(masks_np)

        return cls(
            xyxy=xyxy.astype(np.float32),
            mask=masks_np,
            confidence=np.array(confidences, dtype=np.float32),
            class_id=np.array(class_ids, dtype=int),
        )

    @classmethod
    def from_azure_analyze_image(
        cls, azure_result: dict[str, Any], class_map: dict[int, str] | None = None
    ) -> Detections:
        """
        Creates a Detections instance from [Azure Image Analysis 4.0](
        https://learn.microsoft.com/en-us/azure/ai-services/computer-vision/
        concept-object-detection-40).

        Args:
            azure_result: The result from Azure Image Analysis. It should
                contain detected objects and their bounding box coordinates.
            class_map: A mapping of class IDs to class names. If None, a new
                mapping is created dynamically.

        Returns:
            A new Detections object.

        Example:
            ```python
            import requests
            import supervision as sv

            image = open(input, "rb").read()

            endpoint = "https://.cognitiveservices.azure.com/"
            subscription_key = ""

            headers = {
                "Content-Type": "application/octet-stream",
                "Ocp-Apim-Subscription-Key": subscription_key
             }

            response = requests.post(endpoint,
                headers=self.headers,
                data=image
             ).json()

            detections = sv.Detections.from_azure_analyze_image(response)
            ```
        """
        if "error" in azure_result:
            raise ValueError(
                f"Azure API returned an error {azure_result['error']['message']}"
            )

        xyxy, confidences, class_ids = [], [], []

        is_dynamic_mapping = class_map is None
        if class_map is None:
            class_map = {}

        inverted_map: dict[str, int] = {value: key for key, value in class_map.items()}

        for detection in azure_result["objectsResult"]["values"]:
            bbox = detection["boundingBox"]

            tags = detection["tags"]

            x0 = bbox["x"]
            y0 = bbox["y"]
            x1 = x0 + bbox["w"]
            y1 = y0 + bbox["h"]

            selected_tag: dict[str, Any] | None = None
            selected_class_id: int | None = None
            for tag in sorted(
                tags, key=lambda candidate: candidate["confidence"], reverse=True
            ):
                class_name = tag["name"]
                class_id_val = inverted_map.get(class_name, None)

                if is_dynamic_mapping and class_id_val is None:
                    class_id_val = len(inverted_map)
                    inverted_map[class_name] = class_id_val

                if class_id_val is not None:
                    selected_tag = tag
                    selected_class_id = class_id_val
                    break

            if selected_tag is None:
                if tags:
                    warnings.warn(
                        "Azure detection skipped because none of its tags matched "
                        "the provided class_map.",
                        category=SupervisionWarnings,
                        stacklevel=2,
                    )
                continue

            xyxy.append([x0, y0, x1, y1])
            confidences.append(selected_tag["confidence"])
            class_ids.append(cast(int, selected_class_id))

        if len(xyxy) == 0:
            return Detections.empty()

        return cls(
            xyxy=np.array(xyxy),
            class_id=np.array(class_ids),
            confidence=np.array(confidences),
        )

    @classmethod
    def from_paddledet(cls, paddledet_result: Any) -> Detections:
        """
        Creates a Detections instance from
            [PaddleDetection](https://github.com/PaddlePaddle/PaddleDetection)
            inference result.

        Args:
            paddledet_result: The output Results instance from PaddleDet.

        Returns:
            A new Detections object.

        Example:
            ```python
            import supervision as sv
            import paddle
            from ppdet.engine import Trainer
            from ppdet.core.workspace import load_config

            weights = ()
            config = ()

            cfg = load_config(config)
            trainer = Trainer(cfg, mode='test')
            trainer.load_weights(weights)

            paddledet_result = trainer.predict([images])[0]

            detections = sv.Detections.from_paddledet(paddledet_result)
            ```
        """

        if np.asarray(paddledet_result["bbox"][:, 2:6]).shape[0] == 0:
            return cls.empty()

        return cls(
            xyxy=paddledet_result["bbox"][:, 2:6],
            confidence=paddledet_result["bbox"][:, 1],
            class_id=paddledet_result["bbox"][:, 0].astype(int),
        )

    @classmethod
    def from_lmm(
        cls, lmm: LMM | str, result: str | dict[str, Any], **kwargs: Any
    ) -> Detections:
        """
        !!! deprecated "Deprecated"
            `Detections.from_lmm` is **deprecated** and will be removed in `supervision-0.31.0`.
            Please use `Detections.from_vlm` instead.

        Creates a Detections object from the given result string based on the specified
        Large Multimodal Model (LMM).

        | Name                | Enum (sv.LMM)        | Tasks                   | Required parameters         | Optional parameters |
        |---------------------|----------------------|-------------------------|-----------------------------|---------------------|
        | PaliGemma           | `PALIGEMMA`          | detection               | `resolution_wh`             | `classes`           |
        | PaliGemma 2         | `PALIGEMMA`          | detection               | `resolution_wh`             | `classes`           |
        | Qwen2.5-VL          | `QWEN_2_5_VL`        | detection               | `resolution_wh`, `input_wh` | `classes`           |
        | Qwen3-VL            | `QWEN_3_VL`          | detection               | `resolution_wh`             | `classes`           |
        | Google Gemini 2.0   | `GOOGLE_GEMINI_2_0`  | detection               | `resolution_wh`             | `classes`           |
        | Google Gemini 2.5   | `GOOGLE_GEMINI_2_5`  | detection, segmentation | `resolution_wh`             | `classes`           |
        | Google Gemini 3.5   | `GOOGLE_GEMINI_3_5`  | detection, segmentation | `resolution_wh`             | `classes`           |
        | Moondream           | `MOONDREAM`          | detection               | `resolution_wh`             |                     |
        | DeepSeek-VL2        | `DEEPSEEK_VL_2`      | detection               | `resolution_wh`             | `classes`           |
        | Qwen3-VL            | `QWEN_3_VL`          | detection               | `resolution_wh`             | `classes`           |

        Args:
            lmm: The type of LMM (Large Multimodal Model) to use.
            result: The result string containing the detection data.
            **kwargs: Additional keyword arguments required by the specified LMM.

        Returns:
            A new Detections object.

        Raises:
            ValueError: If the LMM is invalid, required arguments are missing, or
                disallowed arguments are provided.
            ValueError: If the specified LMM is not supported.

        !!! example "PaliGemma"
            ```python

            import supervision as sv

            paligemma_result = "<loc0256><loc0256><loc0768><loc0768> cat"
            detections = sv.Detections.from_lmm(
                sv.LMM.PALIGEMMA,
                paligemma_result,
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog']
            )
            detections.xyxy
            # array([[250., 250., 750., 750.]])

            detections.class_id
            # array([0])

            detections.data
            # {'class_name': array(['cat'], dtype='<U10')}
            ```

        !!! example "Qwen2.5-VL"

            ??? tip "Prompt engineering"

                To get the best results from Qwen2.5-VL, use clear and descriptive prompts
                that specify exactly what you want to detect.

                **For general object detection, use this comprehensive prompt:**

                ```
                Detect all objects in the image and return their locations and labels.
                ```

                **For specific object detection with detailed descriptions:**

                ```
                Detect the red object that is leading in this image and return its location and label.
                ```

                **For simple, targeted detection:**

                ```
                leading blue truck
                ```

                **Additional effective prompts:**

                ```
                Find all people and vehicles in this scene
                ```

                ```
                Locate all animals in the image
                ```

                ```
                Identify traffic signs and their positions
                ```

                **Tips for better results:**

                - Use descriptive language that clearly specifies what to look for
                - Include color, size, or position descriptors when targeting specific objects
                - Be specific about the type of objects you want to detect
                - The model responds well to both detailed instructions and concise phrases
                - Results are returned in JSON format with `bbox_2d` coordinates and `label` fields


            ```python
            import supervision as sv

            qwen_2_5_vl_result = \"\"\"```json
            [
                {"bbox_2d": [139, 768, 315, 954], "label": "cat"},
                {"bbox_2d": [366, 679, 536, 849], "label": "dog"}
            ]
            ```\"\"\"
            detections = sv.Detections.from_lmm(
                sv.LMM.QWEN_2_5_VL,
                qwen_2_5_vl_result,
                input_wh=(1000, 1000),
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog'],
            )
            detections.xyxy
            # array([[139., 768., 315., 954.], [366., 679., 536., 849.]])

            detections.class_id
            # array([0, 1])

            detections.data
            # {'class_name': array(['cat', 'dog'], dtype='<U10')}

            detections.class_id
            # array([0, 1])
            ```

        !!! example "Qwen3-VL"

            ```python
            import supervision as sv

            qwen_3_vl_result = \"\"\"```json
            [
                {"bbox_2d": [139, 768, 315, 954], "label": "cat"},
                {"bbox_2d": [366, 679, 536, 849], "label": "dog"}
            ]
            ```\"\"\"
            detections = sv.Detections.from_lmm(
                sv.LMM.QWEN_3_VL,
                qwen_3_vl_result,
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog'],
            )
            detections.xyxy
            # array([[139., 768., 315., 954.], [366., 679., 536., 849.]])

            detections.class_id
            # array([0, 1])

            detections.data
            # {'class_name': array(['cat', 'dog'], dtype='<U10')}

            detections.class_id
            # array([0, 1])
            ```

        !!! example "Gemini 2.0"

            ??? tip "Prompt engineering"

                From Gemini 2.0 onwards, models are further trained to detect objects in
                an image and get their bounding box coordinates. The coordinates,
                relative to image dimensions, scale to [0, 1000]. You need to convert
                these normalized coordinates back to pixel coordinates using your
                original image size.

                According to the Gemini API documentation on image prompts (see
                https://ai.google.dev/gemini-api/docs/vision#image-input), when using a
                single image with text, the recommended approach is to place the text
                prompt after the image part in the contents array. This ordering has
                been shown to produce significantly better results in practice.

                For example, when calling the Gemini API directly, you can structure
                the request like this, with the image part first and the text prompt
                second in the `parts` list:

                ```json
                {
                  "model": "models/gemini-2.0-flash",
                  "contents": [
                    {
                      "role": "user",
                      "parts": [
                        {
                          "inline_data": {
                            "mime_type": "image/png",
                            "data": "<BASE64_IMAGE_BYTES>"
                          }
                        },
                        {
                          "text": "Detect all the cats and dogs in the image..."
                        }
                      ]
                    }
                  ]
                }
                ```
                To get the best results from Google Gemini 2.0, use the following prompt.

                ```
                Detect all the cats and dogs in the image. The box_2d should be
                [ymin, xmin, ymax, xmax] normalized to 0-1000.
                ```

            ```python
            import supervision as sv

            gemini_response_text = \"\"\"```json
                [
                    {"box_2d": [543, 40, 728, 200], "label": "cat", "id": 1},
                    {"box_2d": [653, 352, 820, 522], "label": "dog", "id": 2}
                ]
            ```\"\"\"

            detections = sv.Detections.from_lmm(
                sv.LMM.GOOGLE_GEMINI_2_0,
                gemini_response_text,
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog'],
            )

            detections.xyxy
            # array([[543., 40., 728., 200.], [653., 352., 820., 522.]])

            detections.data
            # {'class_name': array(['cat', 'dog'], dtype='<U26')}

            detections.class_id
            # array([0, 1])
            ```

        !!! example "Gemini 2.5"

            ??? tip "Prompt engineering"

                To get the best results from Google Gemini 2.5, use the following prompt.

                This prompt is designed to detect all visible objects in the image,
                including small, distant, or partially visible ones, and to return
                tight bounding boxes.

                According to the Gemini API documentation on image prompts, when using
                a single image with text, the recommended approach is to place the text
                prompt after the image part in the `contents` array. See the official
                Gemini vision docs for details:
                https://ai.google.dev/gemini-api/docs/vision#multi-part-input

                For example, using the `google-generativeai` client:

                ```python
                from google.generativeai import types

                response = model.generate_content(
                    contents=[
                        types.Part.from_image(image_bytes),
                        "Carefully examine this image and detect ALL visible objects, including "
                        "small, distant, or partially visible ones.",
                    ],
                    generation_config=generation_config,
                    safety_settings=safety_settings,
                )
                ```

                This ordering (image first, then text) has been shown to produce
                significantly better results in practice.

                ```
                Carefully examine this image and detect ALL visible objects, including
                small, distant, or partially visible ones.

                IMPORTANT: Focus on finding as many objects as possible, even if you are
                only moderately confident.

                Make sure each bounding box is as tight as possible.

                Valid object classes: {class_list}

                For each detected object, provide:
                - "label": the exact class name from the list above
                - "confidence": your certainty (between 0.0 and 1.0)
                - "box_2d": the bounding box [ymin, xmin, ymax, xmax] normalized to 0-1000
                - "mask": the binary mask of the object as a base64-encoded string

                Detect everything that matches the valid classes. Do not be
                conservative; include objects even with moderate confidence.

                Return a JSON array, for example:
                [
                    {
                        "label": "person",
                        "confidence": 0.95,
                        "box_2d": [100, 200, 300, 400],
                        "mask": "..."
                    },
                    {
                        "label": "kite",
                        "confidence": 0.80,
                        "box_2d": [50, 150, 250, 350],
                        "mask": "..."
                    }
                ]
                ```

                When using the google-genai library, it is recommended to set
                thinking_budget=0 in thinking_config for more direct and faster responses.

                ```python
                from google.generativeai import types

                model.generate_content(
                    ...,
                    generation_config=generation_config,
                    safety_settings=safety_settings,
                    thinking_config=types.ThinkingConfig(
                        thinking_budget=0
                    )
                )
                ```

                For a shorter prompt focused only on segmentation masks, you can use:

                ```
                Return a JSON list of segmentation masks. Each entry should include the
                2D bounding box in the "box_2d" key, the segmentation mask in the "mask"
                key, and the text label in the "label" key. Use descriptive labels.
                ```

            ```python
            import supervision as sv

            gemini_response_text = \"\"\"```json
                [
                    {"box_2d": [543, 40, 728, 200], "label": "cat", "id": 1},
                    {"box_2d": [653, 352, 820, 522], "label": "dog", "id": 2}
                ]
            ```\"\"\"

            detections = sv.Detections.from_lmm(
                sv.LMM.GOOGLE_GEMINI_2_5,
                gemini_response_text,
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog'],
            )

            detections.xyxy
            # array([[543., 40., 728., 200.], [653., 352., 820., 522.]])

            detections.data
            # {'class_name': array(['cat', 'dog'], dtype='<U26')}

            detections.class_id
            # array([0, 1])
            ```

        !!! example "Moondream"


            ??? tip "Prompt engineering"

                To get the best results from Moondream, use optimized prompts that leverage
                its object detection capabilities effectively.

                **For general object detection, use this simple prompt:**

                ```
                objects
                ```

                This single-word prompt instructs Moondream to detect all visible objects
                and return them in the proper JSON format with normalized coordinates.


            ```python
            import supervision as sv

            moondream_result = {
                'objects': [
                    {
                        'x_min': 0.5704046934843063,
                        'y_min': 0.20069346576929092,
                        'x_max': 0.7049859315156937,
                        'y_max': 0.3012596592307091
                    },
                    {
                        'x_min': 0.6210969910025597,
                        'y_min': 0.3300672620534897,
                        'x_max': 0.8417936339974403,
                        'y_max': 0.4961046129465103
                    }
                ]
            }

            detections = sv.Detections.from_lmm(
                sv.LMM.MOONDREAM,
                moondream_result,
                resolution_wh=(1000, 1000),
            )

            detections.xyxy
            # array([[1752.28,  818.82, 2165.72, 1229.14],
            #        [1908.01, 1346.67, 2585.99, 2024.11]])
            ```

        !!! example "DeepSeek-VL2"


            ??? tip "Prompt engineering"

                To get the best results from DeepSeek-VL2, use optimized prompts that leverage
                its object detection and visual grounding capabilities effectively.

                **For general object detection, use the following user prompt:**

                ```
                <image>\\n<|ref|>The giraffe at the front<|/ref|>
                ```

                **For visual grounding, use the following user prompt:**

                ```
                <image>\\n<|grounding|>Detect the giraffes
                ```

            ```python
            from PIL import Image
            import supervision as sv

            deepseek_vl2_result = "<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/ref|><|det|>[[26, 31, 632, 998]]<|/det|><|end▁of▁sentence|>"

            detections = sv.Detections.from_vlm(
                vlm=sv.VLM.DEEPSEEK_VL_2, result=deepseek_vl2_result, resolution_wh=image.size
            )

            detections.xyxy
            # array([[ 420,  293,  724,  982],
            #        [  18,   33,  458, 1084]])

            detections.class_id
            # array([0, 1])

            detections.data
            # {'class_name': array(['The giraffe at the back', 'The giraffe at the front'], dtype='<U24')}
            ```
        """  # noqa: E501

        warn_deprecated(
            "`Detections.from_lmm` is deprecated since `supervision-0.26.0` "
            "and will be removed in `supervision-0.31.0`. "
            "Use `Detections.from_vlm` instead."
        )

        # LMM and VLM are mirror enums (identical string values) so value-based
        # lookup is exhaustive by construction — no hand-maintained mapping needed.
        if isinstance(lmm, LMM):
            vlm = VLM(lmm.value)

        elif isinstance(lmm, str):
            try:
                lmm_enum = LMM(lmm.lower())
            except ValueError:
                raise ValueError(
                    f"Invalid LMM string '{lmm}'. Must be one of "
                    f"{[m.value for m in LMM]}"
                )
            vlm = VLM(lmm_enum.value)

        else:
            raise ValueError(
                f"Invalid type for 'lmm': {type(lmm)}. Must be LMM or str."
            )

        return cls.from_vlm(vlm=vlm, result=result, **kwargs)

    @classmethod
    def from_vlm(
        cls, vlm: VLM | str, result: str | dict[str, Any], **kwargs: Any
    ) -> Detections:
        """

        Creates a Detections object from the given result string based on the specified
        Vision Language Model (VLM).

        | Name                | Enum (sv.VLM)        | Tasks                   | Required parameters         | Optional parameters |
        |---------------------|----------------------|-------------------------|-----------------------------|---------------------|
        | PaliGemma           | `PALIGEMMA`          | detection               | `resolution_wh`             | `classes`           |
        | PaliGemma 2         | `PALIGEMMA`          | detection               | `resolution_wh`             | `classes`           |
        | Qwen2.5-VL          | `QWEN_2_5_VL`        | detection               | `resolution_wh`, `input_wh` | `classes`           |
        | Qwen3-VL            | `QWEN_3_VL`          | detection               | `resolution_wh`             | `classes`           |
        | Google Gemini 2.0   | `GOOGLE_GEMINI_2_0`  | detection               | `resolution_wh`             | `classes`           |
        | Google Gemini 2.5   | `GOOGLE_GEMINI_2_5`  | detection, segmentation | `resolution_wh`             | `classes`           |
        | Google Gemini 3.5   | `GOOGLE_GEMINI_3_5`  | detection, segmentation | `resolution_wh`             | `classes`           |
        | Moondream           | `MOONDREAM`          | detection               | `resolution_wh`             |                     |
        | DeepSeek-VL2        | `DEEPSEEK_VL_2`      | detection               | `resolution_wh`             | `classes`           |

        Args:
            vlm: The type of VLM (Vision Language Model) to use.
            result: The result string containing the detection data.
            **kwargs: Additional keyword arguments required by the specified VLM.

        Returns:
            A new Detections object.

        Raises:
            ValueError: If the VLM is invalid, required arguments are missing, or
                disallowed arguments are provided.
            ValueError: If the specified VLM is not supported.

        !!! example "PaliGemma"
            ```python

            import supervision as sv

            paligemma_result = "<loc0256><loc0256><loc0768><loc0768> cat"
            detections = sv.Detections.from_vlm(
                sv.VLM.PALIGEMMA,
                paligemma_result,
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog']
            )
            detections.xyxy
            # array([[250., 250., 750., 750.]])

            detections.class_id
            # array([0])

            detections.data
            # {'class_name': array(['cat'], dtype='<U10')}
            ```

        !!! example "Qwen2.5-VL"

            ??? tip "Prompt engineering"

                To get the best results from Qwen2.5-VL, use clear and descriptive prompts
                that specify exactly what you want to detect.

                **For general object detection, use this comprehensive prompt:**

                ```
                Detect all objects in the image and return their locations and labels.
                ```

                **For specific object detection with detailed descriptions:**

                ```
                Detect the red object that is leading in this image and return its location and label.
                ```

                **For simple, targeted detection:**

                ```
                leading blue truck
                ```

                **Additional effective prompts:**

                ```
                Find all people and vehicles in this scene
                ```

                ```
                Locate all animals in the image
                ```

                ```
                Identify traffic signs and their positions
                ```

                **Tips for better results:**

                - Use descriptive language that clearly specifies what to look for
                - Include color, size, or position descriptors when targeting specific objects
                - Be specific about the type of objects you want to detect
                - The model responds well to both detailed instructions and concise phrases
                - Results are returned in JSON format with `bbox_2d` coordinates and `label` fields


            ```python
            import supervision as sv

            qwen_2_5_vl_result = \"\"\"```json
            [
                {"bbox_2d": [139, 768, 315, 954], "label": "cat"},
                {"bbox_2d": [366, 679, 536, 849], "label": "dog"}
            ]
            ```\"\"\"
            detections = sv.Detections.from_vlm(
                sv.VLM.QWEN_2_5_VL,
                qwen_2_5_vl_result,
                input_wh=(1000, 1000),
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog'],
            )
            detections.xyxy
            # array([[139., 768., 315., 954.], [366., 679., 536., 849.]])

            detections.class_id
            # array([0, 1])

            detections.data
            # {'class_name': array(['cat', 'dog'], dtype='<U10')}

            detections.class_id
            # array([0, 1])
            ```

        !!! example "Qwen3-VL"

            ```python
            import supervision as sv

            qwen_3_vl_result = \"\"\"```json
            [
                {"bbox_2d": [139, 768, 315, 954], "label": "cat"},
                {"bbox_2d": [366, 679, 536, 849], "label": "dog"}
            ]
            ```\"\"\"
            detections = sv.Detections.from_vlm(
                sv.VLM.QWEN_3_VL,
                qwen_3_vl_result,
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog'],
            )
            detections.xyxy
            # array([[139., 768., 315., 954.], [366., 679., 536., 849.]])

            detections.class_id
            # array([0, 1])

            detections.data
            # {'class_name': array(['cat', 'dog'], dtype='<U10')}

            detections.class_id
            # array([0, 1])
            ```

        !!! example "Gemini 2.0"

            ??? tip "Prompt engineering"

                From Gemini 2.0 onwards, models are further trained to detect objects in
                an image and get their bounding box coordinates. The coordinates,
                relative to image dimensions, scale to [0, 1000]. You need to convert
                these normalized coordinates back to pixel coordinates based on your
                original image size.
                According to the [Gemini API documentation on image prompts](
                https://ai.google.dev/gemini-api/docs/vision?lang=python#image_prompts), when using
                a single image with text, the recommended approach is to place the text
                prompt after the image part in the `contents` array (for example,
                `contents=[image_part, text_part]`). This ordering has been shown to
                produce significantly better results in practice.

                To get the best results from Google Gemini 2.0, use the following prompt.

                ```
                Detect all the cats and dogs in the image. The box_2d should be
                [ymin, xmin, ymax, xmax] normalized to 0-1000.
                ```

            ```python
            import supervision as sv

            gemini_response_text = \"\"\"```json
                [
                    {"box_2d": [543, 40, 728, 200], "label": "cat", "id": 1},
                    {"box_2d": [653, 352, 820, 522], "label": "dog", "id": 2}
                ]
            ```\"\"\"

            detections = sv.Detections.from_vlm(
                sv.VLM.GOOGLE_GEMINI_2_0,
                gemini_response_text,
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog'],
            )

            detections.xyxy
            # array([[543., 40., 728., 200.], [653., 352., 820., 522.]])

            detections.data
            # {'class_name': array(['cat', 'dog'], dtype='<U26')}

            detections.class_id
            # array([0, 1])
            ```

        !!! example "Gemini 2.5"

            ??? tip "Prompt engineering"

                To get the best results from Google Gemini 2.5, use the following prompt.

                This prompt is designed to detect all visible objects in the image,
                including small, distant, or partially visible ones, and to return
                tight bounding boxes.

                According to the [Gemini API documentation on image prompts](
                https://ai.google.dev/gemini-api/docs/vision?hl=en),
                when using a single image with text, place the text prompt after the image
                part in the `contents` array. For example, with the `google-genai` client:

                ```python
                response = model.generate_content(
                    [
                        {
                            "role": "user",
                            "parts": [
                                types.Part.from_bytes(image_bytes, mime_type="image/png"),
                                types.Part.from_text(prompt),
                            ],
                        }
                    ]
                )
                ```

                This ordering has been shown to produce significantly better results in practice.

                ```
                Carefully examine this image and detect ALL visible objects, including
                small, distant, or partially visible ones.

                IMPORTANT: Focus on finding as many objects as possible, even if you are
                only moderately confident.

                Make sure each bounding box is as tight as possible.

                Valid object classes: {class_list}

                For each detected object, provide:
                - "label": the exact class name from the list above
                - "confidence": your certainty (between 0.0 and 1.0)
                - "box_2d": the bounding box [ymin, xmin, ymax, xmax] normalized to 0-1000
                - "mask": the binary mask of the object as a base64-encoded string

                Detect everything that matches the valid classes. Do not be
                conservative; include objects even with moderate confidence.

                Return a JSON array, for example:
                [
                    {
                        "label": "person",
                        "confidence": 0.95,
                        "box_2d": [100, 200, 300, 400],
                        "mask": "..."
                    },
                    {
                        "label": "kite",
                        "confidence": 0.80,
                        "box_2d": [50, 150, 250, 350],
                        "mask": "..."
                    }
                ]
                ```

                When using the google-genai library, it is recommended to set
                thinking_budget=0 in thinking_config for more direct and faster responses.

                ```python
                from google.generativeai import types

                model.generate_content(
                    ...,
                    generation_config=generation_config,
                    safety_settings=safety_settings,
                    thinking_config=types.ThinkingConfig(
                        thinking_budget=0
                    )
                )
                ```

                For a shorter prompt focused only on segmentation masks, you can use:

                ```
                Return a JSON list of segmentation masks. Each entry should include the
                2D bounding box in the "box_2d" key, the segmentation mask in the "mask"
                key, and the text label in the "label" key. Use descriptive labels.
                ```

            ```python
            import supervision as sv

            gemini_response_text = \"\"\"```json
                [
                    {"box_2d": [543, 40, 728, 200], "label": "cat", "id": 1},
                    {"box_2d": [653, 352, 820, 522], "label": "dog", "id": 2}
                ]
            ```\"\"\"

            detections = sv.Detections.from_vlm(
                sv.VLM.GOOGLE_GEMINI_2_5,
                gemini_response_text,
                resolution_wh=(1000, 1000),
                classes=['cat', 'dog'],
            )

            detections.xyxy
            # array([[543., 40., 728., 200.], [653., 352., 820., 522.]])

            detections.data
            # {'class_name': array(['cat', 'dog'], dtype='<U26')}

            detections.class_id
            # array([0, 1])
            ```

        !!! example "Moondream"


            ??? tip "Prompt engineering"

                To get the best results from Moondream, use optimized prompts that leverage
                its object detection capabilities effectively.

                **For general object detection, use this simple prompt:**

                ```
                objects
                ```

                This single-word prompt instructs Moondream to detect all visible objects
                and return them in the proper JSON format with normalized coordinates.


            ```python
            import supervision as sv

            moondream_result = {
                'objects': [
                    {
                        'x_min': 0.5704046934843063,
                        'y_min': 0.20069346576929092,
                        'x_max': 0.7049859315156937,
                        'y_max': 0.3012596592307091
                    },
                    {
                        'x_min': 0.6210969910025597,
                        'y_min': 0.3300672620534897,
                        'x_max': 0.8417936339974403,
                        'y_max': 0.4961046129465103
                    }
                ]
            }

            detections = sv.Detections.from_vlm(
                sv.VLM.MOONDREAM,
                moondream_result,
                resolution_wh=(1000, 1000),
            )

            detections.xyxy
            # array([[1752.28,  818.82, 2165.72, 1229.14],
            #        [1908.01, 1346.67, 2585.99, 2024.11]])
            ```

        !!! example "DeepSeek-VL2"


            ??? tip "Prompt engineering"

                To get the best results from DeepSeek-VL2, use optimized prompts that leverage
                its object detection and visual grounding capabilities effectively.

                **For general object detection, use the following user prompt:**

                ```
                <image>\\n<|ref|>The giraffe at the front<|/ref|>
                ```

                **For visual grounding, use the following user prompt:**

                ```
                <image>\\n<|grounding|>Detect the giraffes
                ```

            ```python
            from PIL import Image
            import supervision as sv

            deepseek_vl2_result = "<|ref|>The giraffe at the back<|/ref|><|det|>[[580, 270, 999, 904]]<|/det|><|ref|>The giraffe at the front<|/ref|><|det|>[[26, 31, 632, 998]]<|/det|><|end▁of▁sentence|>"

            detections = sv.Detections.from_vlm(
                vlm=sv.VLM.DEEPSEEK_VL_2, result=deepseek_vl2_result, resolution_wh=image.size
            )

            detections.xyxy
            # array([[ 420,  293,  724,  982],
            #        [  18,   33,  458, 1084]])

            detections.class_id
            # array([0, 1])

            detections.data
            # {'class_name': array(['The giraffe at the back', 'The giraffe at the front'], dtype='<U24')}
            ```

        """  # noqa: E501

        vlm = _validate_vlm_parameters(vlm, result, kwargs)

        if vlm == VLM.PALIGEMMA:
            if not isinstance(result, str):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be str."
                )
            xyxy, class_id, class_name = from_paligemma(result, **kwargs)
            data: _DetectionDataType = {
                CLASS_NAME_DATA_FIELD: class_name,
            }
            return cls(xyxy=xyxy, class_id=class_id, data=data)

        if vlm == VLM.QWEN_2_5_VL:
            if not isinstance(result, str):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be str."
                )
            xyxy, class_id, class_name = from_qwen_2_5_vl(result, **kwargs)
            data = {CLASS_NAME_DATA_FIELD: class_name}
            confidence_arr: npt.NDArray[np.floating[Any]] = np.ones(
                len(xyxy), dtype=float
            )
            return cls(
                xyxy=xyxy, class_id=class_id, confidence=confidence_arr, data=data
            )

        if vlm == VLM.QWEN_3_VL:
            if not isinstance(result, str):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be str."
                )
            xyxy, class_id, class_name = from_qwen_3_vl(result, **kwargs)
            data = {CLASS_NAME_DATA_FIELD: class_name}
            confidence_arr = np.ones(len(xyxy), dtype=float)
            return cls(
                xyxy=xyxy, class_id=class_id, confidence=confidence_arr, data=data
            )

        if vlm == VLM.DEEPSEEK_VL_2:
            if not isinstance(result, str):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be str."
                )
            xyxy, class_id, class_name = from_deepseek_vl_2(result, **kwargs)
            data = {CLASS_NAME_DATA_FIELD: class_name}
            return cls(xyxy=xyxy, class_id=class_id, data=data)

        if vlm == VLM.FLORENCE_2:
            if not isinstance(result, dict):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be dict."
                )
            xyxy, labels, mask, xyxyxyxy = from_florence_2(result, **kwargs)
            if len(xyxy) == 0:
                empty = cls.empty()
                empty.data = {CLASS_NAME_DATA_FIELD: np.empty(0, dtype=str)}
                return empty

            data = {}
            if labels is not None:
                data[CLASS_NAME_DATA_FIELD] = labels
            if xyxyxyxy is not None:
                data[ORIENTED_BOX_COORDINATES] = xyxyxyxy

            return cls(xyxy=xyxy, mask=mask, data=data)

        if vlm == VLM.GOOGLE_GEMINI_2_0:
            if not isinstance(result, str):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be str."
                )
            xyxy, class_id, class_name = from_google_gemini_2_0(result, **kwargs)
            data = {CLASS_NAME_DATA_FIELD: class_name}
            return cls(xyxy=xyxy, class_id=class_id, data=data)

        if vlm == VLM.MOONDREAM:
            if not isinstance(result, dict):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be dict."
                )
            xyxy = from_moondream(result, **kwargs)
            return cls(xyxy=xyxy)

        if vlm == VLM.GOOGLE_GEMINI_2_5:
            if not isinstance(result, str):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be str."
                )
            gemini_result = from_google_gemini_2_5(result, **kwargs)
            data = {CLASS_NAME_DATA_FIELD: gemini_result[2]}
            return cls(
                xyxy=gemini_result[0],
                class_id=gemini_result[1],
                mask=gemini_result[4],
                confidence=gemini_result[3],
                data=data,
            )

        if vlm == VLM.GOOGLE_GEMINI_3_5:
            if not isinstance(result, str):
                raise ValueError(
                    f"Invalid VLM result type: {type(result)}. Must be str."
                )
            gemini_result = from_google_gemini_3_5(result, **kwargs)
            data = {CLASS_NAME_DATA_FIELD: gemini_result[2]}
            return cls(
                xyxy=gemini_result[0],
                class_id=gemini_result[1],
                mask=gemini_result[4],
                confidence=gemini_result[3],
                data=data,
            )

        raise ValueError(f"Unsupported VLM value: {vlm}.")

    @classmethod
    def from_easyocr(cls, easyocr_results: list[Any]) -> Detections:
        """
        Create a Detections object from the
        [EasyOCR](https://github.com/JaidedAI/EasyOCR) result.

        Results are placed in the `data` field with the key `"class_name"`.
        When EasyOCR returns quadrilateral corners, the original corners are
        preserved in ``ORIENTED_BOX_COORDINATES``. Call EasyOCR with
        ``detail=1`` so bounding boxes are available; ``detail=0`` returns text
        strings only and cannot be converted into detections.

        Args:
            easyocr_results: The output Results instance from EasyOCR.

        Returns:
            A new Detections object.

        Example:
            ```python
            import supervision as sv
            import easyocr

            reader = easyocr.Reader(['en'])
            results = reader.readtext("<SOURCE_IMAGE_PATH>")
            detections = sv.Detections.from_easyocr(results)
            detected_text = detections["class_name"]
            ```
        """
        if len(easyocr_results) == 0:
            return cls.empty()

        if isinstance(easyocr_results[0], str):
            raise ValueError(
                "EasyOCR results produced with detail=0 do not include bounding "
                "boxes. Call reader.readtext(..., detail=1) instead."
            )

        bbox = np.array([result[0] for result in easyocr_results], dtype=np.float32)
        if bbox.ndim != 3 or bbox.shape[1:] != (4, 2):
            raise ValueError(
                "EasyOCR results must contain four corner points per detection."
            )
        xyxy = np.hstack((np.min(bbox, axis=1), np.max(bbox, axis=1)))
        confidence = np.array(
            [
                result[2] if len(result) > 2 and result[2] else 0
                for result in easyocr_results
            ]
        )
        ocr_text = np.array([result[1] for result in easyocr_results])

        data: _DetectionDataType = {
            CLASS_NAME_DATA_FIELD: ocr_text,
            ORIENTED_BOX_COORDINATES: bbox,
        }
        return cls(
            xyxy=xyxy.astype(np.float32),
            confidence=confidence.astype(np.float32),
            data=data,
        )

    @classmethod
    def from_ncnn(cls, ncnn_results: Any) -> Detections:
        """
        Creates a Detections instance from the
        [ncnn](https://github.com/Tencent/ncnn) inference result.
        Supports object detection models.

        Args:
            ncnn_results: The output Results instance from ncnn.

        Returns:
            A new Detections object.

        Example:
            ```python
            from supervision import _cv2 as cv2
            from ncnn.model_zoo import get_model
            import supervision as sv

            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            model = get_model(
                "yolov8s",
                target_size=640
                prob_threshold=0.5,
                nms_threshold=0.45,
                num_threads=4,
                use_gpu=True,
                )
            result = model(image)
            detections = sv.Detections.from_ncnn(result)
            ```
        """

        xywh, confidences, class_ids = [], [], []

        if len(ncnn_results) == 0:
            return cls.empty()

        for ncnn_result in ncnn_results:
            rect = ncnn_result.rect
            xywh.append(
                [
                    rect.x.astype(np.float32),
                    rect.y.astype(np.float32),
                    rect.w.astype(np.float32),
                    rect.h.astype(np.float32),
                ]
            )

            confidences.append(ncnn_result.prob)
            class_ids.append(ncnn_result.label)

        return cls(
            xyxy=xywh_to_xyxy(np.array(xywh, dtype=np.float32)),
            confidence=np.array(confidences, dtype=np.float32),
            class_id=np.array(class_ids, dtype=int),
        )

    @classmethod
    def empty(cls) -> Detections:
        """
        Create an empty Detections object with no bounding boxes,
            confidences, or class IDs.

        Returns:
            An empty Detections object.

        Example:
            ```pycon
            >>> from supervision import Detections
            >>> empty_detections = Detections.empty()
            >>> empty_detections.xyxy.shape
            (0, 4)

            ```
        """
        return cls(
            xyxy=np.empty((0, 4), dtype=np.float32),
            confidence=np.array([], dtype=np.float32),
            class_id=np.array([], dtype=int),
        )

    def is_empty(self) -> bool:
        """
        Check whether the `Detections` object has zero bounding boxes.

        Returns:
            `True` if there are no detections, `False` otherwise.

        Examples:
            ```pycon
            >>> import numpy as np
            >>> import supervision as sv
            >>> detections = sv.Detections(
            ...     xyxy=np.array([[10, 20, 110, 120]]),
            ...     class_id=np.array([1]),
            ...     tracker_id=np.array([1]),
            ... )
            >>> filtered = detections[detections.class_id == 99]
            >>> filtered.is_empty()
            True

            ```
        """
        return len(self.xyxy) == 0

    @classmethod
    def merge(cls, detections_list: list[Detections]) -> Detections:
        """
        Merge a list of Detections objects into a single Detections object.

        This method takes a list of Detections objects and combines their
        respective fields (`xyxy`, `mask`, `confidence`, `class_id`, and `tracker_id`)
        into a single Detections object.

        For example, if merging Detections with 3 and 4 detected objects, this method
        will return a Detections with 7 objects (7 entries in `xyxy`, `mask`, etc).

        !!! Note

            When merging, empty `Detections` objects are ignored.

        !!! Note

            **Mask merge policy** — the output mask type follows these rules:

            * All inputs carry
              [`CompactMask`][supervision.detection.compact_mask.CompactMask]
              → result mask is `CompactMask`.
            * Mixed dense `ndarray` + `CompactMask` inputs → dense masks are converted
              to `CompactMask` via
              [`CompactMask.from_dense`][supervision.detection.compact_mask.CompactMask.from_dense];
              result is `CompactMask`. No full `(N, H, W)` stack is allocated.

              !!! warning "Lossy conversion"

                  `from_dense` crops each dense mask to its detection bounding box
                  (`xyxy`). **True pixels outside the bounding box are silently
                  discarded.** This matches the behaviour of
                  `Detections.from_inference(compact_masks=True)`. If pixel-perfect
                  preservation is required, ensure all inputs are already `CompactMask`
                  or use the all-dense path (no `CompactMask` inputs).

            * All inputs carry dense `ndarray` → result is `ndarray` (backward
              compatible).
            * The pairwise merge path used by
              [`with_nms`][supervision.detection.core.Detections.with_nms] /
              [`with_nmm`][supervision.detection.core.Detections.with_nmm]
              (`merge_inner_detection_object_pair`) does **not** preserve `CompactMask`
              — mixed inputs materialise to a dense `ndarray` on that path.

        Args:
            detections_list: A list of Detections objects to merge.

        Returns:
            A single Detections object containing the merged data from the input list.

        Raises:
            ValueError: If some `Detections` have a `mask` and others do not.
            ValueError: If `CompactMask` inputs have different `image_shape` values.
            ValueError: If a dense mask `(H, W)` shape differs from the `CompactMask`
                `image_shape` when mixing mask types.

        Example:
            ```pycon
            >>> import numpy as np
            >>> import supervision as sv
            >>> detections_1 = sv.Detections(
            ...     xyxy=np.array([[15, 15, 100, 100], [200, 200, 300, 300]]),
            ...     class_id=np.array([1, 2]),
            ...     data={'feature_vector': np.array([0.1, 0.2])}
            ... )
            >>> detections_2 = sv.Detections(
            ...     xyxy=np.array([[30, 30, 120, 120]]),
            ...     class_id=np.array([1]),
            ...     data={'feature_vector': np.array([0.3])}
            ... )
            >>> merged_detections = sv.Detections.merge([detections_1, detections_2])
            >>> merged_detections.xyxy
            array([[ 15,  15, 100, 100],
                   [200, 200, 300, 300],
                   [ 30,  30, 120, 120]])
            >>> merged_detections.class_id
            array([1, 2, 1])
            >>> merged_detections.data['feature_vector']
            array([0.1, 0.2, 0.3])

            ```

        Compact mask merge example:

            ```python
            import numpy as np
            import supervision as sv
            from supervision.detection.compact_mask import CompactMask

            H, W = 720, 1280
            masks_a = np.zeros((2, H, W), dtype=bool)
            masks_a[0, 100:200, 100:300] = True
            xyxy_a = np.array([[100., 100., 299., 199.], [400., 300., 600., 500.]])
            cm_a = CompactMask.from_dense(masks_a, xyxy_a, image_shape=(H, W))

            det_compact = sv.Detections(
                xyxy=xyxy_a, mask=cm_a, class_id=np.array([0, 1])
            )

            masks_b = np.zeros((1, H, W), dtype=bool)
            masks_b[0, 50:100, 50:150] = True
            xyxy_b = np.array([[50., 50., 149., 99.]])
            det_dense = sv.Detections(xyxy=xyxy_b, mask=masks_b, class_id=np.array([2]))

            # Dense mask is converted to CompactMask; no (N, H, W) stack allocated.
            merged = sv.Detections.merge([det_compact, det_dense])
            assert isinstance(merged.mask, CompactMask)
            assert len(merged) == 3
            ```
        """
        detections_list = [
            detections for detections in detections_list if not detections.is_empty()
        ]

        if len(detections_list) == 0:
            return Detections.empty()

        for detections in detections_list:
            _validate_detections_fields(
                xyxy=detections.xyxy,
                mask=detections.mask,
                confidence=detections.confidence,
                class_id=detections.class_id,
                tracker_id=detections.tracker_id,
                data=detections.data,
            )

        xyxy = np.vstack([d.xyxy for d in detections_list])

        def stack_mask_or_none() -> npt.NDArray[np.generic] | CompactMask | None:
            masks = [d.mask for d in detections_list]
            if all(m is None for m in masks):
                return None
            if any(m is None for m in masks):
                raise ValueError("All or none of the 'mask' fields must be None")
            if all(isinstance(m, CompactMask) for m in masks):
                return CompactMask.merge(cast(list[CompactMask], masks))
            if all(not isinstance(m, CompactMask) for m in masks):
                # All-dense: preserve backward-compatible dense stacking.
                return cast(
                    npt.NDArray[np.generic], np.vstack([np.asarray(m) for m in masks])
                )
            # Mixed dense and CompactMask: convert dense masks to CompactMask to
            # avoid materialising a full (N, H, W) stack.
            compact_image_shapes = {
                m.image_shape for m in masks if isinstance(m, CompactMask)
            }
            if len(compact_image_shapes) != 1:
                raise ValueError(
                    "Cannot merge CompactMask objects with different image shapes: "
                    f"{sorted(compact_image_shapes)}"
                )
            image_shape: tuple[int, int] = next(iter(compact_image_shapes))
            compact_list: list[CompactMask] = []
            for d, m in zip(detections_list, masks):
                if isinstance(m, CompactMask):
                    compact_list.append(m)
                else:
                    dense = np.asarray(m, dtype=bool)
                    if dense.shape[1:] != image_shape:
                        raise ValueError(
                            f"Dense mask shape {dense.shape[1:]} does not match "
                            f"CompactMask image_shape {image_shape}."
                        )
                    compact_list.append(
                        CompactMask.from_dense(dense, d.xyxy, image_shape)
                    )
            return CompactMask.merge(compact_list)

        def stack_or_none(name: str) -> npt.NDArray[np.generic] | None:
            values = [getattr(d, name) for d in detections_list]
            if all(v is None for v in values):
                return None
            if any(v is None for v in values):
                raise ValueError(f"All or none of the '{name}' fields must be None")
            return cast(npt.NDArray[np.generic], np.hstack(values))

        mask = cast(npt.NDArray[np.bool_] | CompactMask | None, stack_mask_or_none())
        confidence = cast(npt.NDArray[np.floating] | None, stack_or_none("confidence"))
        class_id = cast(npt.NDArray[np.integer] | None, stack_or_none("class_id"))
        tracker_id = cast(npt.NDArray[np.integer] | None, stack_or_none("tracker_id"))

        data = merge_data([d.data for d in detections_list])

        metadata_list = [detections.metadata for detections in detections_list]
        metadata = merge_metadata(metadata_list)

        return cls(
            xyxy=xyxy,
            mask=mask,
            confidence=confidence,
            class_id=class_id,
            tracker_id=tracker_id,
            data=data,
            metadata=metadata,
        )

    def get_anchors_coordinates(self, anchor: Position) -> npt.NDArray[np.generic]:
        """Compute anchor-point coordinates for each detection.

        The anchor can be any position in the `Position` enum, such as
        `CENTER`, `CENTER_LEFT`, `BOTTOM_RIGHT`, etc.

        Selection order:

        1. If ``data[ORIENTED_BOX_COORDINATES]`` is set and ``anchor`` is not
           ``Position.CENTER_OF_MASS``, coordinates are computed from the
           oriented bounding box corners (result lies on the actual rotated
           body).
        2. If ``anchor`` is ``Position.CENTER_OF_MASS``, the detection mask
           centroid is returned regardless of OBB data presence.
        3. Otherwise, the anchor is derived from the axis-aligned envelope
           (``xyxy``).

        Args:
            anchor: Anchor position to compute. Supported positions are
                defined in the `Position` enum.

        Returns:
            Array of shape `(n, 2)` where each row is the `[x, y]` anchor
            coordinate for the corresponding detection.

        Raises:
            ValueError: If the provided `anchor` is not supported.

        Examples:
            Axis-aligned detection:

            ```pycon
            >>> import numpy as np
            >>> import supervision as sv
            >>> detections = sv.Detections(
            ...     xyxy=np.array([[0.0, 0.0, 10.0, 4.0]])
            ... )
            >>> detections.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
            array([[5., 4.]])

            ```

            Oriented (rotated) detection — anchor lies on the rotated body,
            not the axis-aligned envelope:

            ```pycon
            >>> import numpy as np
            >>> import supervision as sv
            >>> corners = np.array(
            ...     [[[0.0, 0.0], [10.0, 0.0], [10.0, 4.0], [0.0, 4.0]]]
            ... )
            >>> detections = sv.Detections(
            ...     xyxy=np.array([[0.0, 0.0, 10.0, 4.0]]),
            ...     data={"xyxyxyxy": corners},
            ... )
            >>> detections.get_anchors_coordinates(sv.Position.BOTTOM_CENTER)
            array([[5., 4.]])

            ```
        """
        if ORIENTED_BOX_COORDINATES in self.data and anchor != Position.CENTER_OF_MASS:
            return cast(
                npt.NDArray[np.generic],
                _oriented_box_anchors(
                    np.asarray(self.data[ORIENTED_BOX_COORDINATES]), anchor
                ),
            )

        xyxy = self.xyxy

        def coordinates(
            x: npt.NDArray[np.number], y: npt.NDArray[np.number]
        ) -> npt.NDArray[np.generic]:
            return cast(npt.NDArray[np.generic], np.array([x, y]).transpose())

        if anchor == Position.CENTER:
            return coordinates(
                (xyxy[:, 0] + xyxy[:, 2]) / 2,
                (xyxy[:, 1] + xyxy[:, 3]) / 2,
            )
        elif anchor == Position.CENTER_OF_MASS:
            if self.mask is None:
                raise ValueError(
                    "Cannot use `Position.CENTER_OF_MASS` without a detection mask."
                )
            return calculate_masks_centroids(masks=self.mask)
        elif anchor == Position.CENTER_LEFT:
            return coordinates(xyxy[:, 0], (xyxy[:, 1] + xyxy[:, 3]) / 2)
        elif anchor == Position.CENTER_RIGHT:
            return coordinates(xyxy[:, 2], (xyxy[:, 1] + xyxy[:, 3]) / 2)
        elif anchor == Position.BOTTOM_CENTER:
            return coordinates((xyxy[:, 0] + xyxy[:, 2]) / 2, xyxy[:, 3])
        elif anchor == Position.BOTTOM_LEFT:
            return coordinates(xyxy[:, 0], xyxy[:, 3])
        elif anchor == Position.BOTTOM_RIGHT:
            return coordinates(xyxy[:, 2], xyxy[:, 3])
        elif anchor == Position.TOP_CENTER:
            return coordinates((xyxy[:, 0] + xyxy[:, 2]) / 2, xyxy[:, 1])
        elif anchor == Position.TOP_LEFT:
            return coordinates(xyxy[:, 0], xyxy[:, 1])
        elif anchor == Position.TOP_RIGHT:
            return coordinates(xyxy[:, 2], xyxy[:, 1])

        raise ValueError(f"{anchor} is not supported.")

    def get_data(self, key: str) -> _DetectionDataValueType | None:
        """Get a value from the detection data dictionary.

        Args:
            key: Data field name.

        Returns:
            The stored data value, or `None` when the key is absent.

        Example:
            ```pycon
            >>> import numpy as np
            >>> from supervision import Detections
            >>> detections = Detections(
            ...     xyxy=np.array([[0, 0, 1, 1]]),
            ...     data={"class_name": np.array(["cat"])},
            ... )
            >>> detections.get_data("class_name").tolist()
            ['cat']

            ```
        """
        return self.data.get(key)

    def select(
        self,
        index: int | np.integer[Any] | slice | list[int] | npt.NDArray[np.generic],
    ) -> Detections:
        """Get a subset of the Detections object.

        Args:
            index: Row index, indices, slice, or boolean mask selecting detections.

        Returns:
            A new `Detections` instance containing the selected rows. Always returns
            a fresh copy — arrays and metadata are never shared with the original,
            even when the selection is empty or the input has zero detections.

        Example:
            ```pycon
            >>> import numpy as np
            >>> from supervision import Detections
            >>> detections = Detections(xyxy=np.array([[0, 0, 1, 1], [1, 1, 2, 2]]))
            >>> detections.select([1]).xyxy.tolist()
            [[1, 1, 2, 2]]

            ```
        """
        mask: npt.NDArray[np.bool_] | CompactMask | None
        if len(self) == 0:
            if isinstance(self.mask, CompactMask):
                mask = self.mask[:0]
            elif self.mask is not None:
                mask = self.mask[:0].copy()
            else:
                mask = None
            data = {
                key: value.copy() if isinstance(value, np.ndarray) else list(value)
                for key, value in self.data.items()
            }
            return Detections(
                xyxy=self.xyxy.copy(),
                mask=mask,
                confidence=(
                    self.confidence.copy() if self.confidence is not None else None
                ),
                class_id=self.class_id.copy() if self.class_id is not None else None,
                tracker_id=(
                    self.tracker_id.copy() if self.tracker_id is not None else None
                ),
                data=data,
                metadata=dict(self.metadata),
            )
        if isinstance(index, (int, np.integer)):
            index = [int(index)]
        array_index = cast(
            slice | list[int] | npt.NDArray[np.integer | np.bool_], index
        )
        data = {
            key: value.copy() if isinstance(value, np.ndarray) else list(value)
            for key, value in get_data_item(self.data, array_index).items()
        }
        if isinstance(self.mask, CompactMask):
            mask = self.mask[cast(Any, array_index)]
        elif self.mask is not None:
            mask = self.mask[cast(Any, array_index)].copy()
        else:
            mask = None
        return Detections(
            xyxy=self.xyxy[array_index].copy(),
            mask=mask,
            confidence=(
                self.confidence[array_index].copy()
                if self.confidence is not None
                else None
            ),
            class_id=(
                self.class_id[array_index].copy() if self.class_id is not None else None
            ),
            tracker_id=(
                self.tracker_id[array_index].copy()
                if self.tracker_id is not None
                else None
            ),
            data=data,
            metadata=dict(self.metadata),
        )

    def __getitem__(
        self,
        index: int
        | np.integer[Any]
        | slice
        | list[int]
        | npt.NDArray[np.generic]
        | str,
    ) -> Detections | list[Any] | npt.NDArray[np.generic] | None:
        """
        Get a subset of the Detections object or access an item from its data field.

        When provided with an integer, slice, list of integers, or a numpy array, this
        method returns a new Detections object that represents a subset of the original
        detections. When provided with a string, it accesses the corresponding item in
        the data dictionary.

        Args:
            index: The index, indices, or key to access a subset of the Detections
                or an item from the data.

        Returns:
            A subset of the Detections object or an item from the data field.

        Example:
            ```python
            import supervision as sv

            detections = sv.Detections()

            first_detection = detections[0]
            first_10_detections = detections[0:10]
            some_detections = detections[[0, 2, 4]]
            class_0_detections = detections[detections.class_id == 0]
            high_confidence_detections = detections[detections.confidence > 0.5]

            feature_vector = detections['feature_vector']
            ```
        """
        if isinstance(index, str):
            return self.get_data(index)
        return self.select(index)

    def __setitem__(self, key: str, value: npt.NDArray[np.generic] | list[Any]) -> None:
        """
        Set a value in the data dictionary of the Detections object.

        Args:
            key: The key in the data dictionary to set.
            value: The value to set for the key.

        Example:
            ```python
            from supervision import _cv2 as cv2
            import supervision as sv
            from ultralytics import YOLO

            image = cv2.imread("<SOURCE_IMAGE_PATH>")
            model = YOLO('yolov8s.pt')

            result = model(image)[0]
            detections = sv.Detections.from_ultralytics(result)

            detections['names'] = [
                 model.model.names[class_id]
                 for class_id
                 in detections.class_id
             ]
            ```

        Raises:
            TypeError: If `value` is not a `np.ndarray` or `list`.
            ValueError: If `value` has a length or shape incompatible with
                the detection count.
        """
        if not isinstance(value, (np.ndarray, list)):
            raise TypeError("Value must be a np.ndarray or a list")

        if isinstance(value, list):
            value = np.array(value)

        _validate_data({key: value}, len(self))
        self.data[key] = value

    @property
    def area(self) -> npt.NDArray[np.generic]:
        """
        Calculate the area of each detection in the set of object detections.

        Selection order:

        1. If ``mask`` is set, return the area of each mask.
        2. Else, if ``data[ORIENTED_BOX_COORDINATES]`` is set, return the area of
           the rotated body (shoelace formula on the four corners).
        3. Otherwise, return the axis-aligned box area (``box_area``).

        **OBB dispatch contract**: presence of ``data[ORIENTED_BOX_COORDINATES]``
        with shape ``(N, 4, 2)`` is the canonical signal that a detection carries
        oriented bounding box geometry. The same presence-of-key check governs
        ``with_nms``, ``with_nmm``, and this property — always store OBB corners
        under ``config.ORIENTED_BOX_COORDINATES`` with that shape.

        **Return dtype**: ``float64`` (OBB branch), input dtype (AABB fallback),
        ``int64`` (mask branch).

        Returns:
            An array containing the area of each detection
                in the format of `(area_1, area_2, ..., area_n)`,
                where n is the number of detections.

        Example:
            ```pycon
            >>> import numpy as np
            >>> import supervision as sv
            >>> corners = np.array(
            ...     [[[0, 5], [5, 10], [10, 5], [5, 0]]], dtype=np.float32
            ... )
            >>> detections = sv.Detections(
            ...     xyxy=np.array([[0, 0, 10, 10]], dtype=np.float32),
            ...     class_id=np.array([0]),
            ...     data={"xyxyxyxy": corners},
            ... )
            >>> detections.area
            array([50.])

            Mask branch returns ``int64`` pixel counts:

            >>> mask = np.zeros((2, 10, 10), dtype=bool)
            >>> mask[0, :3, :3] = True   # 9 pixels
            >>> mask[1, :5, :5] = True   # 25 pixels
            >>> detections = sv.Detections(
            ...     xyxy=np.array(
            ...         [[0, 0, 10, 10], [0, 0, 10, 10]], dtype=np.float32
            ...     ),
            ...     mask=mask,
            ... )
            >>> detections.area
            array([ 9, 25])

            ```
        """
        return detection_area(self)

    @property
    def box_area(self) -> npt.NDArray[np.generic]:
        """
        Calculate the area of each bounding box in the set of object detections.

        Returns:
            An array of floats containing the area of each bounding
                box in the format of `(area_1, area_2, ..., area_n)`,
                where n is the number of detections.
        """
        return (self.xyxy[:, 3] - self.xyxy[:, 1]) * (self.xyxy[:, 2] - self.xyxy[:, 0])

    @property
    def box_aspect_ratio(self) -> npt.NDArray[np.generic]:
        """
        Compute the aspect ratio (width divided by height) for each bounding box.

        Returns:
            Array of shape `(N,)` containing aspect ratios, where `N` is the
                number of boxes (width / height for each box).

        Examples:
            ```python
            import numpy as np
            import supervision as sv

            xyxy = np.array([
                [10, 10, 50, 50],
                [60, 10, 180, 50],
                [10, 60, 50, 180],
            ])

            detections = sv.Detections(xyxy=xyxy)

            detections.box_aspect_ratio
            # array([1.0, 3.0, 0.33333333])

            ar = detections.box_aspect_ratio
            detections[(ar < 2.0) & (ar > 0.5)].xyxy
            # array([[10., 10., 50., 50.]])
            ```
        """
        widths = self.xyxy[:, 2] - self.xyxy[:, 0]
        heights = self.xyxy[:, 3] - self.xyxy[:, 1]

        aspect_ratios = np.full_like(widths, np.nan, dtype=np.float64)
        np.divide(widths, heights, out=aspect_ratios, where=heights != 0)
        return aspect_ratios

    def to_compact_masks(self) -> Detections:
        """Return a copy of this Detections with masks converted to CompactMask.

        The dense :attr:`mask` field (``NDArray[np.bool_]``) is converted to a
        :class:`~supervision.detection.compact_mask.CompactMask` without changing
        mask pixels. When :attr:`mask` is already a
        :class:`~supervision.detection.compact_mask.CompactMask` or is ``None``,
        the instance is returned unchanged.

        Note:
            The crop boundaries are set to the **full image dimensions**, not the
            detector bounding box. No bbox-crop memory savings apply: the RLE
            sparsity still reduces storage versus a dense array, but the
            ``O(bbox_area)`` savings available from
            ``from_inference(..., compact_masks=True)`` are absent here because
            every crop spans the whole frame. Call
            :meth:`~supervision.detection.compact_mask.CompactMask.repack` on the
            resulting mask to tighten crops to their bounding boxes, at the cost
            of potential pixel loss outside those boxes.

        Returns:
            A new :class:`Detections` instance with ``mask`` set to a
            :class:`~supervision.detection.compact_mask.CompactMask`, or ``self``
            when conversion is not needed.

        Example:
            ```python
            import numpy as np
            import supervision as sv
            detections = sv.Detections(
                xyxy=np.array([[0, 0, 10, 10]]),
                mask=np.ones((1, 20, 20), dtype=bool),
            )
            compact = detections.to_compact_masks()
            ```
        """
        from supervision.detection.compact_mask import CompactMask

        if self.mask is None or isinstance(self.mask, CompactMask):
            return self
        image_shape = (int(self.mask.shape[1]), int(self.mask.shape[2]))
        full_image_xyxy = np.tile(
            np.array(
                [[0, 0, image_shape[1] - 1, image_shape[0] - 1]], dtype=np.float64
            ),
            (len(self), 1),
        )
        new = self.__class__(
            xyxy=self.xyxy,
            mask=CompactMask.from_dense(
                masks=self.mask,
                xyxy=full_image_xyxy,
                image_shape=image_shape,
            ),
            confidence=self.confidence,
            class_id=self.class_id,
            tracker_id=self.tracker_id,
            data=self.data,
            metadata=dict(self.metadata),
        )
        return new

    def _build_nms_predictions(
        self, class_agnostic: bool, operation_name: str
    ) -> npt.NDArray[np.floating]:
        """Stack xyxy + confidence (+ class_id) for NMS/NMM/Soft-NMS dispatch.

        Callers must already have verified `self.confidence is not None`.
        """
        if class_agnostic:
            return cast(
                npt.NDArray[np.floating],
                np.hstack(
                    (self.xyxy, cast(np.ndarray, self.confidence).reshape(-1, 1))
                ),
            )
        if self.class_id is None:
            raise ValueError(
                f"Detections class_id must be given for {operation_name} to be "
                f"executed. If you intended to perform class agnostic "
                f"{operation_name} set class_agnostic=True."
            )
        return cast(
            npt.NDArray[np.floating],
            np.hstack(
                (
                    self.xyxy,
                    cast(np.ndarray, self.confidence).reshape(-1, 1),
                    self.class_id.reshape(-1, 1),
                )
            ),
        )

    def with_nms(
        self,
        threshold: float = 0.5,
        class_agnostic: bool = False,
        overlap_metric: OverlapMetric = OverlapMetric.IOU,
    ) -> Detections:
        """
        Performs non-max suppression on detection set. Dispatch order: (1) if mask
        data present, IoU mask is used; (2) else if oriented-box coordinates
        (``data[ORIENTED_BOX_COORDINATES]``) present, oriented-box IoU is used; (3)
        otherwise, axis-aligned box IoU is used.

        Args:
            threshold: The intersection-over-union threshold
                to use for non-maximum suppression. The lower the value the more
                restrictive the NMS becomes. Defaults to 0.5.
            class_agnostic: Whether to perform class-agnostic
                non-maximum suppression. If True, the class_id of each detection
                will be ignored. Defaults to False.
            overlap_metric: Metric used to compute the degree of
                overlap between pairs of masks or boxes (e.g., IoU, IoS).

        Returns:
            A new Detections object containing the subset of detections
                after non-maximum suppression.

        Raises:
            ValueError: If `confidence` is None and class_agnostic is False.
                If `class_id` is None and class_agnostic is False.
        """
        if len(self) == 0:
            return self

        if self.confidence is None:
            raise ValueError(
                "Detections confidence must be given for NMS to be executed."
            )

        predictions = self._build_nms_predictions(class_agnostic, "NMS")

        if self.mask is not None:
            indices = mask_non_max_suppression(
                predictions=predictions,
                masks=self.mask,
                iou_threshold=threshold,
                overlap_metric=overlap_metric,
            )
        elif ORIENTED_BOX_COORDINATES in self.data:
            indices = oriented_box_non_max_suppression(
                predictions=predictions,
                oriented_boxes=np.asarray(
                    self.data[ORIENTED_BOX_COORDINATES], dtype=np.float32
                ),
                iou_threshold=threshold,
                overlap_metric=overlap_metric,
            )
        else:
            indices = box_non_max_suppression(
                predictions=predictions,
                iou_threshold=threshold,
                overlap_metric=overlap_metric,
            )

        return self.select(indices)

    def with_soft_nms(
        self,
        sigma: float = 0.5,
        class_agnostic: bool = False,
        score_threshold: float | None = None,
    ) -> Detections:
        """
        Performs Gaussian Soft Non-Maximum Suppression on detection set. Dispatch
        order: (1) if mask data present, IoU mask is used; (2) otherwise,
        axis-aligned box IoU is used. Oriented-box detections are not given
        dedicated OBB-IoU treatment and fall back to their axis-aligned `xyxy`.

        Unlike `with_nms`, which discards overlapping detections outright,
        Soft-NMS keeps every detection and instead rescales its confidence by
        `score *= exp(-iou**2 / sigma)` for each higher-scoring, same-category
        overlap. By default (`score_threshold=None`) nothing is dropped — the
        method only rescales confidence, despite the "suppression" name; pass
        `score_threshold` to additionally filter the decayed scores into a real
        subset, matching `with_nms`'s return contract.

        Args:
            sigma: Controls the strength of the confidence decay; must be
                greater than `0`. The lower the value the stronger the decay.
                No value of `sigma` reproduces `with_nms`'s hard cutoff —
                Soft-NMS never drops detections on its own.
            class_agnostic: Whether to perform class-agnostic Soft-NMS. If
                True, the class_id of each detection will be ignored.
                Defaults to False.
            score_threshold: If given, detections whose decayed confidence is
                at or below this value are dropped, producing a real subset
                (like `with_nms`). If `None` (default), all detections are
                kept, with their confidence rescaled in place on the returned
                copy.

        Returns:
            A new Detections object with decayed confidence scores and,
                if `score_threshold` is given, filtered to a real subset.
                The original `Detections` instance is never modified.

        Raises:
            ValueError: If `confidence` is None.
                If `class_id` is None and class_agnostic is False.
                If `sigma` is not greater than `0`.
        """
        if len(self) == 0:
            return self

        if self.confidence is None:
            raise ValueError(
                "Detections confidence must be given for Soft-NMS to be executed."
            )

        predictions = self._build_nms_predictions(class_agnostic, "Soft-NMS")

        if self.mask is not None:
            decayed_confidence = mask_soft_non_max_suppression(
                predictions=predictions,
                masks=self.mask,
                sigma=sigma,
            )
        else:
            decayed_confidence = box_soft_non_max_suppression(
                predictions=predictions,
                sigma=sigma,
            )

        result = self.select(np.arange(len(self)))
        result.confidence = decayed_confidence

        if score_threshold is None:
            return result
        return result.select(decayed_confidence > score_threshold)

    def with_nmm(
        self,
        threshold: float = 0.5,
        class_agnostic: bool = False,
        overlap_metric: OverlapMetric = OverlapMetric.IOU,
    ) -> Detections:
        """
        Perform non-maximum merging on the current set of object detections.
        Dispatch order: (1) if mask data present, IoU mask is used; (2) else if
        oriented-box coordinates (``data[ORIENTED_BOX_COORDINATES]``) present,
        oriented-box IoU is used; (3) otherwise, axis-aligned box IoU is used.

        Args:
            threshold: The intersection-over-union threshold
                to use for non-maximum merging. Defaults to 0.5.
            class_agnostic: Whether to perform class-agnostic
                non-maximum merging. If True, the class_id of each detection
                will be ignored. Defaults to False.
            overlap_metric: Metric used to compute the degree of
                overlap between pairs of masks or boxes (e.g., IoU, IoS).

        Returns:
            A new Detections object containing the subset of detections
                after non-maximum merging.

        Note:
            For detections carrying oriented bounding box data
            (``data[ORIENTED_BOX_COORDINATES]``), each merge group's output OBB
            is the tightest rectangle at the winner's orientation enclosing all
            corners contributed by every detection in the group. The winner is
            the highest-confidence detection in the group. The axis-aligned
            ``xyxy`` field is updated to the tight bounding box of that rect.
            For zero-rotation OBBs this equals the axis-aligned union exactly;
            for rotated OBBs the merged rect inherits the winner's rotation angle.
            Groups of size 1 keep the original OBB unchanged.

        Raises:
            ValueError: If `confidence` is None or `class_id` is None and
                class_agnostic is False.

        ![non-max-merging](https://media.roboflow.com/supervision-docs/non-max-merging.png){ align=center width="800" }
        """  # noqa: E501 // docs
        if len(self) == 0:
            return self

        if self.confidence is None:
            raise ValueError(
                "Detections confidence must be given for NMM to be executed."
            )

        predictions = self._build_nms_predictions(class_agnostic, "NMM")

        if self.mask is not None:
            merge_groups = mask_non_max_merge(
                predictions=predictions,
                masks=self.mask,
                iou_threshold=threshold,
                overlap_metric=overlap_metric,
            )
        elif ORIENTED_BOX_COORDINATES in self.data:
            merge_groups = oriented_box_non_max_merge(
                predictions=predictions,
                oriented_boxes=np.asarray(
                    self.data[ORIENTED_BOX_COORDINATES], dtype=np.float32
                ),
                iou_threshold=threshold,
                overlap_metric=overlap_metric,
            )
        else:
            merge_groups = box_non_max_merge(
                predictions=predictions,
                iou_threshold=threshold,
                overlap_metric=overlap_metric,
            )

        result: list[Detections] = []
        for merge_group in merge_groups:
            group = [self.select(i) for i in merge_group]
            result.append(_merge_detection_group(group))

        return Detections.merge(result)


def _merge_obb_corners(
    corners_list: list[npt.NDArray[np.floating]],
) -> npt.NDArray[np.floating]:
    """Merge multiple OBB corner arrays using winner-angle projection.

    The first entry in *corners_list* is the winner. Its orientation angle
    (derived from its first edge) defines the local frame in which the
    tightest enclosing axis-aligned rectangle is computed. That rectangle is
    then rotated back to produce the merged OBB.

    Args:
        corners_list: List of (4, 2) corner arrays. First is the winner.

    Returns:
        Merged OBB corners as a (4, 2) float32 array.
    """
    all_corners = np.concatenate(corners_list, axis=0).astype(np.float32)
    # Use winner's first edge to derive orientation angle -- avoids
    # cv2.minAreaRect surprises (e.g. 90-degree flip for wide rects).
    winner_edge = corners_list[0][1] - corners_list[0][0]
    angle = float(np.arctan2(winner_edge[1], winner_edge[0]))
    cos, sin = float(np.cos(angle)), float(np.sin(angle))

    # De-rotate all corners into the winner's local frame
    to_local = np.array([[cos, -sin], [sin, cos]], dtype=np.float32)
    local_corners = all_corners @ to_local
    x_min = float(local_corners[:, 0].min())
    x_max = float(local_corners[:, 0].max())
    y_min = float(local_corners[:, 1].min())
    y_max = float(local_corners[:, 1].max())

    # Rotate the enclosing AABB back to world frame
    to_world = np.array([[cos, sin], [-sin, cos]], dtype=np.float32)
    merged: npt.NDArray[np.floating] = (
        np.array(
            [[x_min, y_min], [x_max, y_min], [x_max, y_max], [x_min, y_max]],
            dtype=np.float32,
        )
        @ to_world
    )
    return merged


def _merge_detection_group(detections: list[Detections]) -> Detections:
    """Merge a group of single-object Detections into one merged detection.

    Used internally by :meth:`Detections.with_nmm` to combine each merge group
    into a single output detection. The highest-confidence detection is the
    "winner" whose class_id, tracker_id, and data fields are preserved.

    Args:
        detections: List of Detections, each containing exactly one object.

    Returns:
        A single merged Detections object of length 1.
    """
    if len(detections) == 1:
        return detections[0]

    confidences = np.array(
        [d.confidence[0] if d.confidence is not None else 0.0 for d in detections],
        dtype=np.float64,
    )
    winner_idx = int(np.argmax(confidences))
    winner = detections[winner_idx]

    # Area-weighted confidence: each box contributes proportionally to its
    # footprint so large overlapping boxes dominate over small slivers.
    all_xyxy = np.array([d.xyxy[0] for d in detections], dtype=np.float32)
    areas = (all_xyxy[:, 2] - all_xyxy[:, 0]) * (all_xyxy[:, 3] - all_xyxy[:, 1])

    confidence: npt.NDArray[np.floating] | None
    if winner.confidence is not None:
        total_area = float(areas.sum())
        if total_area > 0:
            confidence = np.array(
                [float(np.dot(areas, confidences) / total_area)],
                dtype=np.float32,
            )
        else:
            confidence = winner.confidence
    else:
        confidence = None

    # OBB path: merge corners via winner-angle projection, then derive xyxy
    # from the merged OBB so both stay consistent.
    has_obb = ORIENTED_BOX_COORDINATES in winner.data
    if has_obb:
        corners_list = []
        for det in detections:
            c = np.asarray(det.data[ORIENTED_BOX_COORDINATES])
            if c.ndim != 3 or c.shape[1:] != (4, 2):
                raise ValueError(f"corners must have shape (N, 4, 2); got {c.shape}")
            corners_list.append(c[0].reshape(4, 2))
        merged_corners = _merge_obb_corners(corners_list)
        xyxy = xyxyxyxy_to_xyxy(merged_corners[np.newaxis])
        data = {**winner.data, ORIENTED_BOX_COORDINATES: merged_corners[np.newaxis]}
    else:
        # AABB path: union bounding box across all group members
        xyxy = np.array(
            [
                [
                    all_xyxy[:, 0].min(),
                    all_xyxy[:, 1].min(),
                    all_xyxy[:, 2].max(),
                    all_xyxy[:, 3].max(),
                ]
            ],
            dtype=np.float32,
        )
        data = winner.data

    # Mask union via logical OR. Preserve CompactMask outputs without re-cropping
    # to the merged box, because source masks may legitimately extend outside it.
    masks = [d.mask for d in detections if d.mask is not None]
    if masks:
        if all(isinstance(m, CompactMask) for m in masks):
            compact_masks = cast(list[CompactMask], masks)
            image_shape = compact_masks[0].image_shape
            if any(m.image_shape != image_shape for m in compact_masks):
                raise ValueError(
                    "Cannot merge CompactMask objects with different image shapes."
                )
            union_mask = np.zeros(image_shape, dtype=bool)
            for compact_mask in compact_masks:
                union_mask |= compact_mask.to_dense()[0]
            union_xyxy = mask_to_xyxy(union_mask[np.newaxis]).astype(np.float32)
            mask = CompactMask.from_dense(
                masks=union_mask[np.newaxis],
                xyxy=union_xyxy,
                image_shape=image_shape,
            )
        else:
            dense_masks = [
                m.to_dense() if isinstance(m, CompactMask) else m for m in masks
            ]
            mask = np.logical_or.reduce(np.concatenate(dense_masks, axis=0))[np.newaxis]
    else:
        mask = None

    metadata = merge_metadata([d.metadata for d in detections])

    return Detections(
        xyxy=xyxy,
        mask=mask,
        confidence=confidence,
        class_id=winner.class_id,
        tracker_id=winner.tracker_id,
        data=data,
        metadata=metadata,
    )


@deprecated(deprecated_in="0.29.0", remove_in="0.32.0")  # type: ignore[untyped-decorator]
def merge_inner_detection_object_pair(
    detections_1: Detections, detections_2: Detections
) -> Detections:
    """
    Merges two Detections objects into a single Detections object.
    Assumes each Detections contains exactly one object.

    A `winning` detection is determined based on the confidence score of the two
    input detections. This winning detection is then used to specify which
    `class_id`, `tracker_id`, and `data` to include in the merged Detections object.

    The resulting `confidence` of the merged object is calculated by the weighted
    contribution of each detection to the merged object.
    The bounding boxes and masks of the two input detections are merged into a
    single bounding box and mask, respectively.

    Args:
        detections_1: The first Detections object.
        detections_2: The second Detections object.

    Returns:
        A new Detections object, with merged attributes.

    Raises:
        ValueError: If the input Detections objects do not have exactly 1 detected
            object.

    Example:
        ```python
        from supervision import _cv2 as cv2
        import supervision as sv
        from inference import get_model

        image = cv2.imread("<SOURCE_IMAGE_PATH>")
        model = get_model(model_id="yolov8s-640")

        result = model.infer(image)[0]
        detections = sv.Detections.from_inference(result)

        merged_detections = merge_object_detection_pair(
            detections[0], detections[1])
        ```
    """
    if len(detections_1) != 1 or len(detections_2) != 1:
        raise ValueError("Both Detections should have exactly 1 detected object.")

    _validate_fields_both_defined_or_none(detections_1, detections_2)

    xyxy_1 = detections_1.xyxy[0]
    xyxy_2 = detections_2.xyxy[0]
    if detections_1.confidence is None and detections_2.confidence is None:
        merged_confidence = None
    else:
        assert detections_1.confidence is not None
        assert detections_2.confidence is not None
        detection_1_area = (xyxy_1[2] - xyxy_1[0]) * (xyxy_1[3] - xyxy_1[1])
        detections_2_area = (xyxy_2[2] - xyxy_2[0]) * (xyxy_2[3] - xyxy_2[1])
        merged_confidence = (
            detection_1_area * detections_1.confidence[0]
            + detections_2_area * detections_2.confidence[0]
        ) / (detection_1_area + detections_2_area)
        merged_confidence = np.array([merged_confidence])

    merged_x1, merged_y1 = np.minimum(xyxy_1[:2], xyxy_2[:2])
    merged_x2, merged_y2 = np.maximum(xyxy_1[2:], xyxy_2[2:])
    merged_xyxy = np.array([[merged_x1, merged_y1, merged_x2, merged_y2]])

    if detections_1.mask is None and detections_2.mask is None:
        merged_mask = None
    else:
        merged_mask = np.logical_or(
            cast(npt.NDArray[Any], detections_1.mask),
            cast(npt.NDArray[Any], detections_2.mask),
        )

    if detections_1.confidence is None or detections_2.confidence is None:
        winning_detection = detections_1
    elif detections_1.confidence[0] >= detections_2.confidence[0]:
        winning_detection = detections_1
    else:
        winning_detection = detections_2

    metadata = merge_metadata([detections_1.metadata, detections_2.metadata])

    return Detections(
        xyxy=merged_xyxy,
        mask=merged_mask,
        confidence=merged_confidence,
        class_id=winning_detection.class_id,
        tracker_id=winning_detection.tracker_id,
        data=winning_detection.data,
        metadata=metadata,
    )


@deprecated(deprecated_in="0.29.0", remove_in="0.32.0")  # type: ignore[untyped-decorator]
def merge_inner_detections_objects(
    detections: list[Detections],
    threshold: float = 0.5,
    overlap_metric: OverlapMetric = OverlapMetric.IOU,
) -> Detections:
    """
    Given N detections each of length 1 (exactly one object inside), combine them into a
    single detection object of length 1. The contained inner object will be the merged
    result of all the input detections.

    For example, this lets you merge N boxes into one big box, N masks into one mask,
    etc.
    """
    detections_1 = detections[0]
    for detections_2 in detections[1:]:
        iou = detection_iou(detections_1, detections_2, overlap_metric)[0]
        if iou < threshold:
            break
        detections_1 = merge_inner_detection_object_pair(detections_1, detections_2)
    return detections_1


@deprecated(deprecated_in="0.29.0", remove_in="0.32.0")  # type: ignore[untyped-decorator]
def merge_inner_detections_objects_without_iou(
    detections: list[Detections],
) -> Detections:
    """
    Given N detections each of length 1 (exactly one object inside), combine them into a
    single detection object of length 1. The contained inner object will be the merged
    result of all the input detections.

    For example, this lets you merge N boxes into one big box, N masks into one mask,
    etc.
    """
    return reduce(merge_inner_detection_object_pair, detections)


def _validate_fields_both_defined_or_none(
    detections_1: Detections, detections_2: Detections
) -> None:
    """
    Verify that for each optional field in the Detections, both instances either have
    the field set to None or both have it set to non-None values.

    `data` field is ignored.

    Raises:
        ValueError: If one field is None and the other is not, for any of the fields.
    """
    attributes = get_instance_variables(detections_1)
    for attribute in attributes:
        value_1 = getattr(detections_1, attribute)
        value_2 = getattr(detections_2, attribute)

        if (value_1 is None) != (value_2 is None):
            raise ValueError(
                f"Field '{attribute}' should be consistently None or not None in both "
                "Detections."
            )


@deprecated(  # type: ignore[untyped-decorator]
    target=_validate_fields_both_defined_or_none,
    deprecated_in="0.29.0",
    remove_in="0.32.0",
)
def validate_fields_both_defined_or_none(
    detections_1: Detections, detections_2: Detections
) -> None:
    void(detections_1, detections_2)
