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: 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. aram start: Point : The starting point of the line. aram 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. aram bbox: np.ndarray : The bounding box of the player. aram 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. aram 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. aram frame: np.ndarray : The image on which the line and annotations will be drawn. aram 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...