[Python] How to make a proper text display for real time video analytics

Discussão em 'Python' iniciado por Stack, Fevereiro 26, 2025.

  1. Stack

    Stack Membro Participativo

    I’m new to video analytics using YOLO and OpenCV. I found a code example from this post: Juggling Counting Using YOLOv8 and modified it with some additional features using DeepSeek AI https://www.linkedin.com/pulse/juggling-counting-using-yolov8-vriza-wahyu-saputra-sbswc%3FtrackingId=QhklwV3dSqCMwTkAlHbqVw%253D%253D/.

    The problem I’m facing is overlapping text annotations in the output video. The text keeps stacking on top of itself every frame instead of updating properly, making it unreadable. Here's a screenshot of the issue:

    [​IMG]

    main.py

    from supervision.draw.color import ColorPalette
    from supervision.video.dataclasses import VideoInfo
    from supervision.video.source import get_video_frames_generator
    from supervision.video.sink import VideoSink
    from supervision.tools.detections import Detections, BoxAnnotator
    from utils import LineCounter, LineCounterAnnotator
    from tqdm.notebook import tqdm
    import numpy as np
    import cv2
    from ultralytics import YOLO
    from supervision.geometry.dataclasses import Point

    # Load models
    MODEL_POSE = "yolov8n-pose.pt"
    model_pose = YOLO(MODEL_POSE)

    MODEL = "yolov8n.pt"
    model = YOLO(MODEL)

    # Dictionary mapping class_id to class_name
    CLASS_NAMES_DICT = model.model.names
    # Class ID for the ball
    CLASS_ID = [32]

    # Define source and target video paths
    SOURCE_VIDEO_PATH = "datasets/juggling.mp4"
    TARGET_VIDEO_PATH = "datasets/juggling_result.mp4"

    # Create VideoInfo instance
    video_info = VideoInfo.from_video_path(SOURCE_VIDEO_PATH)
    # Create frame generator
    generator = get_video_frames_generator(SOURCE_VIDEO_PATH)
    # Create instance of BoxAnnotator and LineCounterAnnotator
    box_annotator = BoxAnnotator(color=ColorPalette(), thickness=1, text_thickness=1, text_scale=0.5)
    # Create LineCounter instance
    line_counter = LineCounter(start=Point(0, 0), end=Point(0, 0))
    # Create instance of LineCounterAnnotator
    line_annotator = LineCounterAnnotator(thickness=3, text_thickness=3, text_scale=2)

    # Open target video file
    with VideoSink(TARGET_VIDEO_PATH, video_info) as sink:
    # Loop over video frames
    for frame in tqdm(generator, total=video_info.total_frames):
    # Perform pose detection
    results_poses = model_pose.track(frame, persist=True)
    annotated_frame = results_poses[0].plot()
    keypoints = results_poses[0].keypoints.xy.int().cpu().tolist()
    bboxes = results_poses[0].boxes.xyxy.cpu().numpy()

    # Perform ball detection
    results_ball = model.track(frame, persist=True, conf=0.1)
    tracker_ids = results_ball[0].boxes.id.int().cpu().numpy() if results_ball[0].boxes.id is not None else None
    detections = Detections(
    xyxy=results_ball[0].boxes.xyxy.cpu().numpy(),
    confidence=results_ball[0].boxes.conf.cpu().numpy(),
    class_id=results_ball[0].boxes.cls.cpu().numpy().astype(int),
    tracker_id=tracker_ids
    )

    # Filter out detections with unwanted classes
    mask = np.array([class_id in CLASS_ID for class_id in detections.class_id], dtype=bool)
    detections.filter(mask=mask, inplace=True)

    # Update line position based on player's knees
    line_counter.update_line(bboxes[0], keypoints[0])
    # Update line counter with ball detections
    line_counter.update(detections=detections)

    # Annotate and display frame
    labels = [
    f"id:{track_id} {CLASS_NAMES_DICT[class_id]} {confidence:0.2f}"
    for _, confidence, class_id, track_id
    in detections
    ]
    annotated_frame = box_annotator.annotate(frame=annotated_frame, detections=detections, labels=labels)
    annotated_frame = line_annotator.annotate(frame=annotated_frame, line_counter=line_counter)

    # Display the annotated frame
    cv2.imshow("Real-Time Soccer Juggling Evaluation", annotated_frame)
    # Write the annotated frame to the output video
    sink.write_frame(annotated_frame)

    # Break the loop if 'q' is pressed
    if cv2.waitKey(1) & 0xFF == ord("q"):
    break

    # Release resources
    cv2.destroyAllWindows()


    utils.py

    from typing import Dict, Set, List
    import cv2
    import numpy as np
    from supervision.draw.color import Color
    from supervision.geometry.dataclasses import Point, Rect, Vector
    from supervision.tools.detections import Detections


    class LineCounter:
    def __init__(self, start: Point, end: Point):
    """
    Initialize a LineCounter object.

    :param start: Point : The starting point of the line.
    :param end: Point : The ending point of the line.
    """
    self.vector = Vector(start=start, end=end)
    self.tracker_state: Dict[str, bool] = {}
    self.in_count: int = 0
    self.ball_count: int = 0 # Counter for ball crossings
    self.detected_players: Set[int] = set() # Unique player IDs detected
    self.current_player_count: int = 0 # Players in the current frame
    self.player_metrics: Dict[int, dict] = {} # Metrics for each player

    def update_line(self, bbox: np.ndarray, keypoint: np.ndarray):
    """
    Update the line position based on the player's knees.

    :param bbox: np.ndarray : The bounding box of the player.
    :param keypoint: np.ndarray : The keypoints of the player.
    """
    left_knee = keypoint[13]
    right_knee = keypoint[14]
    max_y_knee = min(left_knee[1], right_knee[1])

    line_start = Point(bbox[0], max_y_knee)
    line_end = Point(bbox[2], max_y_knee)

    self.vector = Vector(start=line_start, end=line_end)

    def update(self, detections: Detections):
    """
    Update the in_count for the detections that cross the line.

    :param detections: Detections : The detections for which to update the counts.
    """
    current_frame_players = set()
    current_time = cv2.getTickCount() / cv2.getTickFrequency() # Current timestamp

    for xyxy, confidence, class_id, tracker_id in detections:
    if tracker_id is None:
    continue

    # Initialize player metrics if not already present
    if tracker_id not in self.player_metrics:
    self.player_metrics[tracker_id] = {
    "juggle_count": 0,
    "touch_times": [],
    "last_touch_time": None,
    "ball_proximity": [],
    "skill_level": 1,
    }

    current_frame_players.add(tracker_id)
    self.detected_players.add(tracker_id)

    x1, y1, x2, y2 = xyxy
    anchors = [
    Point(x=x1, y=y1),
    Point(x=x1, y=y2),
    Point(x=x2, y=y1),
    Point(x=x2, y=y2),
    ]
    triggers = [self.vector.is_in(point=anchor) for anchor in anchors]

    if len(set(triggers)) == 2:
    self.tracker_state[tracker_id] = False
    continue

    tracker_state = triggers[0]
    if tracker_id not in self.tracker_state:
    self.tracker_state[tracker_id] = tracker_state
    continue

    if self.tracker_state.get(tracker_id) == tracker_state:
    continue

    self.tracker_state[tracker_id] = tracker_state
    if tracker_state:
    self.in_count += 1

    # Update player skill levels
    for player_id, metrics in self.player_metrics.items():
    juggle_count = metrics["juggle_count"]
    timing_consistency = (
    np.std(metrics["touch_times"]) if metrics["touch_times"] else float("inf")
    )
    average_proximity = (
    np.mean(metrics["ball_proximity"]) if metrics["ball_proximity"] else float("inf")
    )

    # Define skill level thresholds
    if juggle_count > 50 and timing_consistency < 0.2 and average_proximity < 50:
    metrics["skill_level"] = 5
    elif juggle_count > 30 and timing_consistency < 0.5 and average_proximity < 100:
    metrics["skill_level"] = 4
    elif juggle_count > 20 and timing_consistency < 0.8 and average_proximity < 150:
    metrics["skill_level"] = 3
    elif juggle_count > 10 and timing_consistency < 1.0 and average_proximity < 200:
    metrics["skill_level"] = 2
    else:
    metrics["skill_level"] = 1

    self.current_player_count = len(current_frame_players)


    class LineCounterAnnotator:
    def __init__(
    self,
    thickness: float = 2,
    color: Color = Color.white(),
    text_thickness: float = 2,
    text_color: Color = Color.black(),
    text_scale: float = 0.5,
    text_offset: float = 1.5,
    text_padding: int = 10,
    ):
    """
    Initialize the LineCounterAnnotator object with default values.
    """
    self.thickness: float = thickness
    self.color: Color = color
    self.text_thickness: float = text_thickness
    self.text_color: Color = text_color
    self.text_scale: float = text_scale
    self.text_offset: float = text_offset
    self.text_padding: int = text_padding

    def annotate(self, frame: np.ndarray, line_counter: LineCounter) -> np.ndarray:
    """
    Draws the line and annotations on the frame.

    :param frame: np.ndarray : The image on which the line and annotations will be drawn.
    :param line_counter: LineCounter : The line counter that will be used to draw the line and annotations.
    :return: np.ndarray : The image with the line and annotations drawn on it.
    """
    # Create a copy of the frame to avoid modifying the original
    annotated_frame = frame.copy()

    # Draw the counting line
    cv2.line(
    annotated_frame,
    line_counter.vector.start.as_xy_int_tuple(),
    line_counter.vector.end.as_xy_int_tuple(),
    self.color.as_bgr(),
    self.thickness,
    lineType=cv2.LINE_AA,
    )

    # Display general statistics
    in_text = f"Crossing Players: {line_counter.in_count} | Total Players: {len(line_counter.detected_players)}"
    ball_text = f"Ball Count: {line_counter.ball_count}"
    frame_player_text = f"Players in Frame: {line_counter.current_player_count}"

    # Display general statistics at the top-left corner
    cv2.putText(annotated_frame, in_text, (10, 30),
    cv2.FONT_HERSHEY_SIMPLEX, self.text_scale,
    self.text_color.as_bgr(), self.text_thickness, cv2.LINE_AA)
    cv2.putText(annotated_frame, ball_text, (10, 60),
    cv2.FONT_HERSHEY_SIMPLEX, self.text_scale,
    self.text_color.as_bgr(), self.text_thickness, cv2.LINE_AA)
    cv2.putText(annotated_frame, frame_player_text, (10, 90),
    cv2.FONT_HERSHEY_SIMPLEX, self.text_scale,
    self.text_color.as_bgr(), self.text_thickness, cv2.LINE_AA)

    # Display player-specific metrics in a clean, non-overlapping manner
    y_offset = 120 # Start displaying player metrics below the general statistics
    for player_id, metrics in line_counter.player_metrics.items():
    skill_level_text = {
    1: "Beginner",
    2: "Novice",
    3: "Intermediate",
    4: "Advanced",
    5: "Expert",
    }.get(metrics["skill_level"], "Unknown")

    player_text = (
    f"Player {player_id}: "
    f"Juggles={metrics['juggle_count']}, "
    f"Skill Level={skill_level_text}"
    )
    cv2.putText(annotated_frame, player_text, (10, y_offset),
    cv2.FONT_HERSHEY_SIMPLEX, self.text_scale,
    self.text_color.as_bgr(), self.text_thickness, cv2.LINE_AA)
    y_offset += 30 # Move down for the next player's metrics

    # Display additional feedback (timing and proximity)
    feedback_text = f"Timing: {np.std(metrics['touch_times']):.2f}s, Proximity: {np.mean(metrics['ball_proximity']):.2f}px"
    cv2.putText(annotated_frame, feedback_text, (10, y_offset),
    cv2.FONT_HERSHEY_SIMPLEX, self.text_scale * 0.8,
    self.text_color.as_bgr(), self.text_thickness, cv2.LINE_AA)
    y_offset += 30 # Move down for the next player's feedback

    return annotated_frame


    What I’ve Tried:

    • Ensuring cv2.putText() is called only once per frame
    • Checking if previous frames are cleared properly before drawing new text
    • Adjusting text position dynamically

    Continue reading...

Compartilhe esta Página