Repository: roboflow/supervision

Issue:
Add annotator `sv.PercentageBarAnnotator` which shows percentage bar of confidence level in specified position
### Search before asking

- [X] I have searched the Supervision [issues](https://github.com/roboflow/supervision/issues) and found no similar feature requests.


### Description

I have created a new annotator class named `PercentageBarAnnotator` which draws a percentage bar based on the confidence for each prediction, in a specific position around the box.

An issue already exists, issue #459 , but it seems to be inactive since it's 2+ months old. I had posted there a demo video from a previous try to implement that but it was not pretty and uncompleted.

`PercentageBarAnnotator` accepts the following attributes:

```
-   height: int = 14,
-   width: int = 80,
-   color: Union[Color, ColorPalette] = ColorPalette.default(),
-   border_color: Color = Color.black(),
-   position: Position = Position.TOP_CENTER,
-   color_lookup: ColorLookup = ColorLookup.CLASS,
```

`percentage_bar_annotator = sv.PercentageBarAnnotator()`


Below is a video demo of the annotator. Let me know your thoughts about it, for any possible changes, and if its good I can submit a PR.

https://github.com/roboflow/supervision/assets/65918758/e214fab1-ba12-4479-95cd-33cfcd42fcd6


Some more examples regarding the height attribute of the percentage bar:
`
sv.PercentageBarAnnotator(height=12)`

![percentage_bar_height_12](https://github.com/roboflow/supervision/assets/65918758/ac0874b8-8607-4cda-8c6a-5c91cd7d9648)


`sv.PercentageBarAnnotator(height=22)`

![percentage_bar_height_22](https://github.com/roboflow/supervision/assets/65918758/089f4ee2-b17d-47f8-a187-197afb7caff8)




### Use case

_No response_

### Additional

_No response_

### Are you willing to submit a PR?

- [X] Yes I'd like to help by submitting a PR!


Discussion:
Hi @xaristeidou 👋🏻 ! I love your progress. I think you should open PR with what you have already.

Resolving patch:
diff --git a/supervision/__init__.py b/supervision/__init__.py
--- a/supervision/__init__.py
+++ b/supervision/__init__.py
@@ -18,6 +18,7 @@
     HeatMapAnnotator,
     LabelAnnotator,
     MaskAnnotator,
+    PercentageBarAnnotator,
     PixelateAnnotator,
     PolygonAnnotator,
     RoundBoxAnnotator,
diff --git a/supervision/annotators/core.py b/supervision/annotators/core.py
--- a/supervision/annotators/core.py
+++ b/supervision/annotators/core.py
@@ -1436,7 +1436,7 @@ def annotate(
             ```
 
         ![round-box-annotator-example](https://media.roboflow.com/
-        supervision-annotator-examples/round-box-annotator-example.png)
+        supervision-annotator-examples/round-box-annotator-example-purple.png)
         """
 
         for detection_idx in range(len(detections)):
@@ -1496,3 +1496,177 @@ def annotate(
                 )
 
         return scene
+
+
+class PercentageBarAnnotator(BaseAnnotator):
+    """
+    A class for drawing percentage bars on an image using provided detections.
+    """
+
+    def __init__(
+        self,
+        height: int = 16,
+        width: int = 80,
+        color: Union[Color, ColorPalette] = ColorPalette.default(),
+        border_color: Color = Color.black(),
+        position: Position = Position.TOP_CENTER,
+        color_lookup: ColorLookup = ColorLookup.CLASS,
+        border_thickness: int = None,
+    ):
+        """
+        Args:
+            height (int): The height in pixels of the percentage bar.
+            width (int): The width in pixels of the percentage bar.
+            color (Union[Color, ColorPalette]): The color or color palette to use for
+                annotating detections.
+            border_color (Color): The color of the border lines.
+            position (Position): The anchor position of drawing the percentage bar.
+            color_lookup (str): Strategy for mapping colors to annotations.
+                Options are `INDEX`, `CLASS`, `TRACK`.
+            border_thickness (int): The thickness of the border lines.
+        """
+        self.height: int = height
+        self.width: int = width
+        self.color: Union[Color, ColorPalette] = color
+        self.border_color: Color = border_color
+        self.position: Position = position
+        self.color_lookup: ColorLookup = color_lookup
+
+        if border_thickness is None:
+            self.border_thickness = int(0.15 * self.height)
+
+    def annotate(
+        self,
+        scene: np.ndarray,
+        detections: Detections,
+        custom_color_lookup: Optional[np.ndarray] = None,
+        custom_values: Optional[np.ndarray] = None,
+    ) -> np.ndarray:
+        """
+        Annotates the given scene with percentage bars based on the provided
+        detections. The percentage bars visually represent the confidence or custom
+        values associated with each detection.
+
+        Args:
+            scene (np.ndarray): The image where percentage bars will be drawn.
+            detections (Detections): Object detections to annotate.
+            custom_color_lookup (Optional[np.ndarray]): Custom color lookup array.
+                Allows to override the default color mapping strategy.
+            custom_values (Optional[np.ndarray]): Custom values array to use instead
+                of the default detection confidences. This array should have the
+                same length as the number of detections and contain a value between
+                0 and 1 (inclusive) for each detection, representing the percentage
+                to be displayed.
+
+        Returns:
+            The annotated image.
+
+        Example:
+            ```python
+            >>> import supervision as sv
+
+            >>> image = ...
+            >>> detections = sv.Detections(...)
+
+            >>> percentage_bar_annotator = sv.BoundingBoxAnnotator()
+            >>> annotated_frame = percentage_bar_annotator.annotate(
+            ...     scene=image.copy(),
+            ...     detections=detections
+            ... )
+            ```
+
+        ![percentage-bar-example](https://media.roboflow.com/
+        supervision-annotator-examples/percentage-bar-annotator-example-purple.png)
+        """
+        self.validate_custom_values(
+            custom_values=custom_values, detections_count=len(detections)
+        )
+        anchors = detections.get_anchors_coordinates(anchor=self.position)
+        for detection_idx in range(len(detections)):
+            anchor = anchors[detection_idx]
+            border_coordinates = self.calculate_border_coordinates(
+                anchor_xy=(int(anchor[0]), int(anchor[1])),
+                border_wh=(self.width, self.height),
+                position=self.position,
+            )
+            border_width = border_coordinates[1][0] - border_coordinates[0][0]
+
+            value = (
+                custom_values[detection_idx]
+                if custom_values is not None
+                else detections.confidence[detection_idx]
+            )
+
+            color = resolve_color(
+                color=self.color,
+                detections=detections,
+                detection_idx=detection_idx,
+                color_lookup=self.color_lookup
+                if custom_color_lookup is None
+                else custom_color_lookup,
+            )
+            cv2.rectangle(
+                img=scene,
+                pt1=border_coordinates[0],
+                pt2=(
+                    border_coordinates[0][0] + int(border_width * value),
+                    border_coordinates[1][1],
+                ),
+                color=color.as_bgr(),
+                thickness=-1,
+            )
+            cv2.rectangle(
+                img=scene,
+                pt1=border_coordinates[0],
+                pt2=border_coordinates[1],
+                color=self.border_color.as_bgr(),
+                thickness=self.border_thickness,
+            )
+        return scene
+
+    @staticmethod
+    def calculate_border_coordinates(
+        anchor_xy: Tuple[int, int], border_wh: Tuple[int, int], position: Position
+    ) -> Tuple[Tuple[int, int], Tuple[int, int]]:
+        cx, cy = anchor_xy
+        width, height = border_wh
+
+        if position == Position.TOP_LEFT:
+            return (cx - width, cy - height), (cx, cy)
+        elif position == Position.TOP_CENTER:
+            return (cx - width // 2, cy), (cx + width // 2, cy - height)
+        elif position == Position.TOP_RIGHT:
+            return (cx, cy), (cx + width, cy - height)
+        elif position == Position.CENTER_LEFT:
+            return (cx - width, cy - height // 2), (cx, cy + height // 2)
+        elif position == Position.CENTER or position == Position.CENTER_OF_MASS:
+            return (
+                (cx - width // 2, cy - height // 2),
+                (cx + width // 2, cy + height // 2),
+            )
+        elif position == Position.CENTER_RIGHT:
+            return (cx, cy - height // 2), (cx + width, cy + height // 2)
+        elif position == Position.BOTTOM_LEFT:
+            return (cx - width, cy), (cx, cy + height)
+        elif position == Position.BOTTOM_CENTER:
+            return (cx - width // 2, cy), (cx + width // 2, cy + height)
+        elif position == Position.BOTTOM_RIGHT:
+            return (cx, cy), (cx + width, cy + height)
+
+    @staticmethod
+    def validate_custom_values(
+        custom_values: Optional[Union[np.ndarray, List[float]]], detections_count: int
+    ) -> None:
+        if custom_values is not None:
+            if not isinstance(custom_values, (np.ndarray, list)):
+                raise TypeError(
+                    "custom_values must be either a numpy array or a list of floats."
+                )
+
+            if len(custom_values) != detections_count:
+                raise ValueError(
+                    "The length of custom_values must match the number of detections."
+                )
+
+            if not all(0 <= value <= 1 for value in custom_values):
+                raise ValueError("All values in custom_values must be between 0 and 1.")


Test patch: