Visualization#

This quick tutorial introduces the key concepts and basic features of visualization.

%load_ext autoreload
%autoreload 2
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload
import sportslabkit as slk

root = slk.utils.get_git_root()
dataset_path = slk.datasets.get_path('wide_view')
path_to_csv = sorted(dataset_path.glob('annotations/*.csv'))[0]
path_to_mp4 = sorted(dataset_path.glob('videos/*.mp4'))[0]

df = slk.load_df(path_to_csv)
df.head()
TeamID 0 ... 1 3
PlayerID 1 10 ... 9 0
Attributes bb_left bb_top bb_width bb_height conf bb_left bb_top bb_width bb_height conf ... bb_left bb_top bb_width bb_height conf bb_left bb_top bb_width bb_height conf
frame
0 3543.0 607.0 30.0 52.5 1.0 3536.42 555.93 13.57 42.39 1.0 ... 2919.31 538.44 23.59 47.18 1.0 3542.77 549.47 6.4 7.0 1.0
1 3542.0 609.0 32.0 51.0 1.0 3536.13 555.96 13.66 42.27 1.0 ... 2919.44 538.55 23.59 47.18 1.0 3548.55 549.43 6.4 7.0 1.0
2 3542.0 611.0 32.0 50.0 1.0 3535.85 555.99 13.73 42.16 1.0 ... 2919.57 538.66 23.59 47.18 1.0 3554.32 549.40 6.4 7.0 1.0
3 3542.0 613.0 32.0 49.0 1.0 3535.57 556.02 13.80 42.04 1.0 ... 2919.70 538.77 23.59 47.18 1.0 3560.10 549.36 6.4 7.0 1.0
4 3539.0 615.0 36.0 46.0 1.0 3535.28 556.04 13.88 41.94 1.0 ... 2919.84 538.88 23.59 47.18 1.0 3565.87 549.33 6.4 7.0 1.0

5 rows × 115 columns

from sportslabkit.utils import cv2pil

cam = slk.Camera(path_to_mp4)
frame = cam.get_frame(1)

h, w = frame.shape[:2]
cv2pil(frame, convert_bgr2rgb=False).resize((w//6, h//6))
../../_images/e48ae35fbe32a9f3b9e95297d0013586ed9e20c26d1a5cd540748781790ed68f.png
df.visualize_frame(0, frame)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[9], line 1
----> 1 df.visualize_frame(0, frame)

File ~/Github/SportsLabKit/sportslabkit/dataframe/bboxdataframe.py:123, in BBoxDataFrame.visualize_frame(self, frame_idx, frame, draw_frame_id, use_cool_viz)
    121 label = f"{team_id}_{player_id}"
    122 player_id_int = sum([int(x) for x in str(hash(player_id))[1:]])
--> 123 color = _COLOR_NAMES[hash(player_id_int) % len(_COLOR_NAMES)]
    125 logger.debug(f"x1: {x1}, y1: {y1}, x2: {x2}, y2: {y2}, label: {label}, color: {color}")
    126 frame = add_bbox_to_frame(frame, x1, y1, x2, y2, label, color)

NameError: name '_COLOR_NAMES' is not defined
import cv2

import json
data_path = '/Users/atom/Github/SoccerTrack/soccertrack/datasets/fisheye_keypoints.json'

with open(data_path) as f:
    keypoints = json.load(f)

def draw_f(frame):
    draw = frame.copy()
    # draw = cv2.cvtColor(draw, cv2.COLOR_RGB2BGR)

    p1 = keypoints['(0.0, 0.0)']
    p2 = keypoints['(0.0, 68.0)']
    p3 = keypoints['(105.0, 0.0)']
    p4 = keypoints['(105.0, 68.0)']

    # draw the four corners
    for p in [p1, p2, p3, p4]:
        x, y = int(p[0]), int(p[1])
        cv2.circle(draw, (x, y), 25, (255, 0, 0), -1)
    cv2.line(draw, (int(p1[0]), int(p1[1])), (int(p2[0]), int(p2[1])), (255, 0, 0), 5)
    cv2.line(draw, (int(p1[0]), int(p1[1])), (int(p3[0]), int(p3[1])), (255, 0, 0), 5)
    cv2.line(draw, (int(p2[0]), int(p2[1])), (int(p4[0]), int(p4[1])), (255, 0, 0), 5)
    cv2.line(draw, (int(p3[0]), int(p3[1])), (int(p4[0]), int(p4[1])), (255, 0, 0), 5)

    for s, keypoint in keypoints.items():
        x, y = int(keypoint[0]), int(keypoint[1])
        cv2.circle(draw, (x, y), 5, (0, 255, 0), -1)
    return draw

wide_view_mp4 = 'overlay_ball.mp4'
cam = Camera(wide_view_mp4)

cam._seek(0)
slk.utils.make_video((draw_f(frame) for frame in cam), 'test_with_lines_2.mp4', input_framerate=cam.frame_rate, custom_ffmpeg='/opt/homebrew/bin/ffmpeg')
# cv2pil(draw).resize((w//6, h//6))
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
Cell In[4], line 6
      3 import json
      4 data_path = '/Users/atom/Github/SoccerTrack/soccertrack/datasets/fisheye_keypoints.json'
----> 6 with open(data_path) as f:
      7     keypoints = json.load(f)
      9 def draw_f(frame):

File ~/miniconda3/envs/sportslabkit-test/lib/python3.10/site-packages/IPython/core/interactiveshell.py:284, in _modified_open(file, *args, **kwargs)
    277 if file in {0, 1, 2}:
    278     raise ValueError(
    279         f"IPython won't let you open fd={file} by default "
    280         "as it is likely to crash IPython. If you know what you are doing, "
    281         "you can use builtins' open."
    282     )
--> 284 return io_open(file, *args, **kwargs)

FileNotFoundError: [Errno 2] No such file or directory: '/Users/atom/Github/SoccerTrack/soccertrack/datasets/fisheye_keypoints.json'
import cv2
from soccertrack.utils import make_video

import json
data_path = '/Users/atom/Github/SoccerTrack/soccertrack/datasets/fisheye_keypoints.json'

with open(data_path) as f:
    keypoints = json.load(f)

def draw_f(frame):
    draw = frame.copy()
    # draw = cv2.cvtColor(draw, cv2.COLOR_RGB2BGR)

    p1 = keypoints['(0.0, 0.0)']
    p2 = keypoints['(0.0, 68.0)']
    p3 = keypoints['(105.0, 0.0)']
    p4 = keypoints['(105.0, 68.0)']

    # draw the four corners
    for p in [p1, p2, p3, p4]:
        x, y = int(p[0]), int(p[1])
        cv2.circle(draw, (x, y), 25, (255, 0, 0), -1)
    cv2.line(draw, (int(p1[0]), int(p1[1])), (int(p2[0]), int(p2[1])), (255, 0, 0), 5)
    cv2.line(draw, (int(p1[0]), int(p1[1])), (int(p3[0]), int(p3[1])), (255, 0, 0), 5)
    cv2.line(draw, (int(p2[0]), int(p2[1])), (int(p4[0]), int(p4[1])), (255, 0, 0), 5)
    cv2.line(draw, (int(p3[0]), int(p3[1])), (int(p4[0]), int(p4[1])), (255, 0, 0), 5)

    for s, keypoint in keypoints.items():
        x, y = int(keypoint[0]), int(keypoint[1])
        cv2.circle(draw, (x, y), 5, (0, 255, 0), -1)
    return draw

wide_view_mp4 = 'overlay_ball.mp4'
cam = Camera(wide_view_mp4)

cam._seek(0)
make_video((draw_f(frame) for frame in cam), 'test_with_lines_2.mp4', input_framerate=cam.frame_rate, custom_ffmpeg='/opt/homebrew/bin/ffmpeg')
# cv2pil(draw).resize((w//6, h//6))

Plotting a single frame#

cam = slk.Camera(wide_view_mp4)
frame = cam.get_frame(1)
cv2pil(df.visualize_frame(frame_idx=1, frame=frame))
../../_images/d62999615e50d355b86a55bab39148d232e2719108a62199556d8707e253c355.png
frame = cam.get_frame(1)
rect = Rect(10, 10, 4286, 42)

color = Color(0, 255, 0)
cv2pil(draw_ellipse(frame, rect, color, 2))
../../_images/7e2b2d420e4c8c94cb4c703f63264e9a81a7174ba22677b7a51529fd4a5e39ad.png
MARKER_CONTOUR_COLOR_HEX = "000000"
MARKER_CONTOUR_COLOR = Color.from_hex_string(MARKER_CONTOUR_COLOR_HEX)

# red
PLAYER_MARKER_FILL_COLOR_HEX = "FF0000"
PLAYER_MARKER_FILL_COLOR = Color.from_hex_string(PLAYER_MARKER_FILL_COLOR_HEX)

# green
BALL_MERKER_FILL_COLOR_HEX = "00FF00"
BALL_MARKER_FILL_COLOR = Color.from_hex_string(BALL_MERKER_FILL_COLOR_HEX)

MARKER_CONTOUR_THICKNESS = 2
MARKER_WIDTH = 20
MARKER_HEIGHT = 20
MARKER_MARGIN = 10

# distance in pixels from the player's bounding box where we consider the ball is in his possession
PLAYER_IN_POSSESSION_PROXIMITY = 30


# calculates coordinates of possession marker
def calculate_marker(anchor: Point) -> np.ndarray:
    x, y = anchor.int_xy_tuple
    return(np.array([
        [x - MARKER_WIDTH // 2, y - MARKER_HEIGHT - MARKER_MARGIN],
        [x, y - MARKER_MARGIN],
        [x + MARKER_WIDTH // 2, y - MARKER_HEIGHT - MARKER_MARGIN]
    ]))


# draw single possession marker
def draw_marker(image: np.ndarray, anchor: Point, color: Color) -> np.ndarray:
    possession_marker_countour = calculate_marker(anchor=anchor)
    image = draw_filled_polygon(
        image=image, 
        countour=possession_marker_countour, 
        color=color)
    image = draw_polygon(
        image=image, 
        countour=possession_marker_countour, 
        color=MARKER_CONTOUR_COLOR,
        thickness=MARKER_CONTOUR_THICKNESS)
    return image
# Colors are in BGR format
# Orange
BALL_COLOR_HEX = "#005AFF"
BALL_COLOR = Color.from_hex_string(BALL_COLOR_HEX)

# red
GOALKEEPER_1_COLOR_HEX =  "#0000FF"
GOALKEEPER_COLOR_1 = Color.from_hex_string(GOALKEEPER_1_COLOR_HEX)

# yellow
GOALKEEPER_2_COLOR_HEX = "#00FFFF"
GOALKEEPER_COLOR_2 = Color.from_hex_string(GOALKEEPER_2_COLOR_HEX)

# Sky Blue
PLAYER_1_COLOR_HEX = "#C0C0C0"
PLAYER_COLOR_1 = Color.from_hex_string(PLAYER_1_COLOR_HEX)

# Silver
PLAYER_2_COLOR_HEX = "#FFFF00"
PLAYER_COLOR_2 = Color.from_hex_string(PLAYER_2_COLOR_HEX)


COLORS = [
    BALL_COLOR,
    GOALKEEPER_COLOR_1,
    GOALKEEPER_COLOR_2,
    PLAYER_COLOR_1,
    PLAYER_COLOR_2,
]
THICKNESS = 4

CLASS_BALL = 0
CLASS_GOALKEEPER_1 = 1
CLASS_GOALKEEPER_2 = 2
CLASS_PLAYER_1 = 3
CLASS_PLAYER_2= 4

# initiate annotators
annotator = BaseAnnotator(
    colors=COLORS, 
    thickness=THICKNESS)

text_annotator = TextAnnotator(background_color=Color(255, 255, 255), text_color=Color(0, 0, 0), text_thickness=2)
ball_marker_annotator = MarkerAnntator(color=BALL_COLOR)
# player_marker_annotator = MarkerAnntator(color=PLAYER_MARKER_FILL_COLOR)
def plot_frame(frame_number, _frame):
    
    frame = _frame.copy()
    
    detections = []
    for (teamd_id, player_id), _detection in df[df.index == frame_number].iter_players():
        team_id = int(teamd_id) if teamd_id != 'BALL' else 3
        player_id = int(player_id) if player_id != 'BALL' else 0

        if _detection.empty:
            continue
        x_min, y_min, w, h, confidence = _detection[['bb_left',  'bb_top',  'bb_width',  'bb_height',  'conf']].values.squeeze()
        x_max = x_min + w
        y_max = y_min + h

        if team_id == 0:
            class_id = CLASS_PLAYER_1 if player_id != 0 else CLASS_GOALKEEPER_1 
        elif team_id == 1:
            class_id = CLASS_PLAYER_2 if player_id != 10 else CLASS_GOALKEEPER_2
        elif team_id == 3:
            class_id = CLASS_BALL
        else:
            raise ValueError(f"Unknown team id: {team_id}")

        det = Detection(
            rect=Rect(
                x=float(x_min),
                y=float(y_min),
                width=float(x_max - x_min),
                height=float(y_max - y_min),
            ),
            tracker_id=player_id,
            class_id=class_id,
            class_name='',
            confidence=float(confidence),
        )
        detections.append(det)

    player_detections = list(filter(lambda d: d.class_id !=CLASS_BALL, detections))
    ball_detections = list(filter(lambda d: d.class_id ==CLASS_BALL, detections))
    
    # annotate video frame
    annotated_image = annotator.annotate(image=frame, detections=player_detections)
    annotated_image = text_annotator.annotate(image=annotated_image, detections=player_detections)
    annotated_image = ball_marker_annotator.annotate(image=annotated_image, detections=ball_detections)

    return annotated_image


cv2pil(plot_frame(1, cam.get_frame(1)), convert_bgr2rgb=False)
../../_images/a6ea910e096d56ae53cebb3512f1f383c769c8a34f33670b673f5d552fccca1b.png
slk.utils.make_video((plot_frame(i+1, frame[:,:,::-1]) for i, frame in enumerate(cam.iter_frames())), 'assets/cool_vis.mp4', input_framerate=25)
Writing video: 74it [00:04, 14.84it/s]
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[163], line 1
----> 1 slk.utils.make_video((plot_frame(i+1, frame[:,:,::-1]) for i, frame in enumerate(cam.iter_frames())), 'assets/cool_vis.mp4', input_framerate=25)

File ~/Github/SoccerTrack/sportslabkit/utils/utils.py:296, in make_video(frames, outpath, vcodec, pix_fmt, preset, crf, ss, t, c, height, width, input_framerate, logging, custom_ffmpeg)
    287 writer = WriteGear(
    288     output=outpath,
    289     compression_mode=True,
   (...)
    292     **output_params,
    293 )
    295 # loop over
--> 296 for frame in tqdm(frames, desc=f"Writing video", level="INFO"):
    297     writer.write(frame, rgb_mode=True)  # activate RGB Mode
    299 writer.close()

File ~/miniconda3/envs/soccertrack-test-env/lib/python3.10/site-packages/tqdm/std.py:1178, in tqdm.__iter__(self)
   1175 time = self._time
   1177 try:
-> 1178     for obj in iterable:
   1179         yield obj
   1180         # Update and possibly print the progressbar.
   1181         # Note: does not call self.update(1) for speed optimisation.

Cell In[163], line 1, in <genexpr>(.0)
----> 1 slk.utils.make_video((plot_frame(i+1, frame[:,:,::-1]) for i, frame in enumerate(cam.iter_frames())), 'assets/cool_vis.mp4', input_framerate=25)

Cell In[124], line 3, in plot_frame(frame_number, _frame)
      1 def plot_frame(frame_number, _frame):
----> 3     frame = _frame.copy()
      5     detections = []
      6     for (teamd_id, player_id), _detection in df[df.index == frame_number].iter_players():

KeyboardInterrupt: 
import cv2
import pandas as pd

def write_dataframe_on_image(df, image, start_pos=(10, 10), font_scale=0.5, color=(255, 255, 255)):
    # Define the starting position
    x, y = start_pos

    # Loop through DataFrame and write text on the image
    counter = 1
    for index, row in df.iterrows():
        TeamID = index[1]
        PlayerID = index[2]
        
        text = f"TID: {TeamID} PID: {PlayerID}, x: {row['bb_top']:.2f}, y: {row['bb_left']:.2f}, w: {row['bb_width']:.2f}, h: {row['bb_height']:.2f}"
        cv2.putText(image, text, (x, int(y + counter * 30 * font_scale * 1.3)), cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, 2)
        counter += 1

    return image

# Example usage:
# for i, frame in enumerate(cam.iter_frames()):
#     frame = plot_frame(i+1, frame[:,:,::-1])  # Ensure plot_frame is defined
#     frame_df = df[df.index == i+1].to_long_df()  # Ensure to_long_df is defined
#     image = write_dataframe_on_image(frame_df, frame, start_pos=(50, 50), font_scale=1, color=(20,255,83))
    
def f(i, frame):
    frame = plot_frame(i+1, frame[:,:,::-1])  # Ensure plot_frame is defined
    frame_df = df[df.index == i+1].to_long_df()  # Ensure to_long_df is defined
    image = write_dataframe_on_image(frame_df, frame, start_pos=(1000, 50), font_scale=1, color=(20,255,83))
    return image

cam = slk.Camera(wide_view_mp4)
slk.utils.make_video((f(i, frame) for i, frame in enumerate(cam.iter_frames())), "video.mp4", input_framerate=25)
Writing video: 750it [01:20,  9.30it/s]
# dedicated annotator to draw possession markers on video frames
def draw_circle(image, center, radius, color, thickness=1):
    return cv2.circle(
        img=image, 
        center=center, 
        radius=radius, 
        color=color, 
        thickness=thickness)

@dataclass
class TrackAnnotator:
    
    def __init__(self, color) -> None:
        self.color = color
        self.centers = []
    
    def annotate(self, image: np.ndarray, detections: List[Detection]) -> np.ndarray:
        annotated_image = image.copy()
        for detection in detections:
            # FIXME: This breaks if there are multiple detections
            annotated_image = draw_circle(
                image=image,
                center=detection.rect.bottom_center.int_xy_tuple,
                radius=int(detection.rect.width * 1.5),
                color=self.color.bgr_tuple,
                thickness=3
            )
        
            # calculate text dimensions
            size, _ = cv2.getTextSize(
                "Ball Detected!!", 
                cv2.FONT_HERSHEY_SIMPLEX, 
                1.7, 
                thickness=4)
            width, height = size
            
            # calculate text background position
            center_x, center_y = detection.rect.bottom_center.int_xy_tuple
            x = center_x - width // 2 + 350
            y = center_y - height // 2 + 10 - 75
            
            # draw background
            background_color = COLORS[detection.class_id]
            annotated_image = draw_filled_rect(
                image=annotated_image, 
                rect=Rect(x=x, y=y, width=width * 1, height=height * 1).pad(padding=25), 
                color=background_color)
            
            annotated_image = draw_filled_rect(
                image=annotated_image, 
                rect=Rect(x=x, y=y, width=width, height=height).pad(padding=15), 
                color=Color.from_hex_string("#202529"))
            
            # draw text
            annotated_image = cv2.putText(image, "Ball Detected!!", Point(x=x, y=y + height).int_xy_tuple, cv2.FONT_HERSHEY_SIMPLEX, 1.7, Color.from_hex_string("#ffffff").bgr_tuple, 4, 2, False)
            
            # draw horizontal line from text 
            bx, by = detection.rect.bottom_center.int_xy_tuple
            tx, ty = Point(x=x, y=y + height).int_xy_tuple
            
            # make the line a bit shorter
            tx = bx + int((tx - bx) * 0.8)
            ty = by + int((ty - by) * 0.8)
            
            bx = bx + int((tx - bx) * 0.2)
            by = by + int((ty - by) * 0.2)
            
            annotated_image = cv2.line(
                img=annotated_image, 
                pt1=(bx, by),
                pt2=(tx, ty),
                color=self.color.bgr_tuple,
                thickness=3)
                        
            if self.centers:
                for i, center in enumerate(self.centers):
                    annotated_image = draw_circle(
                        image=image,
                        center=center,
                        radius=int(detection.rect.width * 0.5 * (1 - (len(self.centers) - i) / len(self.centers))),
                        color=self.color.bgr_tuple,
                        thickness=int(3 * (1 - (len(self.centers) - i) / len(self.centers)))
                    )
            
            
            self.centers.append(detection.rect.bottom_center.int_xy_tuple)
            self.centers = self.centers[-50:]
        return annotated_image


def plot_ball(frame_number, _frame):
    frame = _frame.copy()
    
    detections = []
    for (teamd_id, player_id), _detection in df[df.index == frame_number].iter_players():
        team_id = int(teamd_id) if teamd_id != 'BALL' else 3
        player_id = int(player_id) if player_id != 'BALL' else 0
        
        if _detection.empty:
            continue
        x_min, y_min, w, h, confidence = _detection.values.squeeze()
        x_max = x_min + w
        y_max = y_min + h

        if team_id == 0:
            class_id = CLASS_PLAYER_1 if player_id != 0 else CLASS_GOALKEEPER_1 
        elif team_id == 1:
            class_id = CLASS_PLAYER_2 if player_id != 0 else CLASS_GOALKEEPER_2
        elif team_id == 3:
            class_id = CLASS_BALL
        else:
            raise ValueError(f"Unknown team id: {team_id}")

        det = Detection(
            rect=Rect(
                x=float(x_min),
                y=float(y_min),
                width=float(x_max - x_min),
                height=float(y_max - y_min),
            ),
            tracker_id=player_id,
            class_id=class_id,
            class_name='',
            confidence=float(confidence),
        )
        detections.append(det)

    ball_detections = list(filter(lambda d: d.class_id ==CLASS_BALL, detections))
    

    # annotate video frame
    annotated_image = track_annotator.annotate(image=frame, detections=ball_detections)
    return annotated_image

track_annotator = TrackAnnotator(color=BALL_COLOR)
cv2pil(plot_ball(1, cam.get_frame(0)), convert_bgr2rgb=False)
../../_images/550233302b22893ab2a555e92b1dcee8c7cdff7f735ebbe70768dec2c0a53269.png
from soccertrack.utils.utils import make_video

make_video((plot_ball(i+1, frame) for i, frame in enumerate(cam.iter_frames())), 'test.mp4', input_framerate=25)
Writing video: 750it [01:05, 11.45it/s]
def plot_frame(frame_number, _frame):
    frame = _frame.copy()
    
    detections = []
    for (teamd_id, player_id), _detection in df[df.index == frame_number].iter_players():
        team_id = int(teamd_id)
        player_id = int(player_id)
        
        if _detection.empty:
            continue
        x_min, y_min, w, h, confidence = _detection.values.squeeze()
        x_max = x_min + w
        y_max = y_min + h

        if team_id == 0:
            class_id = CLASS_PLAYER_1 if player_id != 0 else CLASS_GOALKEEPER_1 
        elif team_id == 1:
            class_id = CLASS_PLAYER_2 if player_id != 0 else CLASS_GOALKEEPER_2
        elif team_id == 3:
            class_id = CLASS_BALL
        else:
            raise ValueError(f"Unknown team id: {team_id}")

        det = Detection(
            rect=Rect(
                x=float(x_min),
                y=float(y_min),
                width=float(x_max - x_min),
                height=float(y_max - y_min),
            ),
            tracker_id=player_id,
            class_id=class_id,
            class_name='',
            confidence=float(confidence),
        )
        detections.append(det)
    
    # HD size bounding box
    box = Rect(x=0, y=0, width=1280, height=720)

    if not detections:
        frame = frame[box.y:box.y + box.height, box.x:box.x + box.width]
        return frame
    ball_detection = list(filter(lambda d: d.class_id ==CLASS_BALL, detections))[0]
    
    
    # center the box on the ball but keep it within the frame
    bx = int(min(max(ball_detection.rect.center.x - box.width / 2, 0), frame.shape[1] - box.width))
    by = int(min(max(ball_detection.rect.center.y - box.height / 2, 0), frame.shape[0] - box.height))
    box = Rect(x=bx, y=by, width=box.width, height=box.height)
    
    # Mask out the box but keep the background visible with a bit of transparency
    opacity = 0.5
    mask = np.zeros(frame.shape, dtype=np.uint8)
    mask = cv2.rectangle(mask, (box.x, box.y), (box.x + box.width, box.y + box.height), (255, 255, 255), -1)
    
    masked_frame = cv2.bitwise_and(frame, mask)
    masked_frame = cv2.addWeighted(masked_frame, opacity, frame, 1 - opacity, 0)
    
    if 1:
        # crop the frame to the box
        masked_frame = masked_frame[box.y:box.y + box.height, box.x:box.x + box.width]
    
    return masked_frame

cv2pil(plot_frame(1, cam.get_frame(0)))
../../_images/d2907e932feba8b926f26f64a59dd20ea2d3c939c5653c0566558a1774599127.png
make_video((plot_frame(i+1, frame) for i, frame in enumerate(cam.iter_frames())), 'test.mp4', input_framerate=25)
Writing video: 750it [00:23, 31.36it/s]

Plotting a multiple frames#

from pathlib import Path

path_to_mp4 = Path('/Users/atom/Downloads') / 'A031C0024_20230211201927_0005.mp4'
path_to_mp4.exists()
True
import soccertrack
from soccertrack import detection_model
from soccertrack.utils import get_git_root
from soccertrack import Camera

root = get_git_root()
cam = Camera(path_to_mp4) # Camera object will be used to load frames
frame = cam[50]

model_name = "yolov5"
model_repo =  root / "external" / "yolov5"
model_ckpt = root / "models" / "yolov5" / "yolov5m.pt"

det_model = detection_model.load(model_name, model_repo, model_ckpt)

det_model.model.conf = 0.01
det_model.model.max_det=30

det_result = det_model(frame, size = 1080)

print(det_result.to_df())
det_result.show(width=5)
YOLOv5 🚀 v6.2-239-gf33718f Python-3.10.9 torch-1.13.1 CPU

Fusing layers... 
YOLOv5m summary: 290 layers, 21172173 parameters, 0 gradients
Adding AutoShape... 
      bbox_left    bbox_top  bbox_width  bbox_height      conf  class
0   1360.600342  574.979919   52.461426   127.136475  0.862603    0.0
1    919.093811  381.701691   20.472778    55.379272  0.759889    0.0
2    983.026978  339.392700   15.257019    46.538910  0.574201    0.0
3    677.153809  371.058777   12.505981    34.700684  0.540997    0.0
4    786.935303  341.576477   11.174805    26.958282  0.533679    0.0
5   1680.884888  660.867188   52.098389    85.228455  0.507503    0.0
6    998.585815  328.584778   12.681824    27.968323  0.425303    0.0
7    612.843567  357.670410   10.662842    29.959564  0.421751    0.0
8    494.011841  400.206360   12.484100    35.074371  0.392153    0.0
9    402.424957  659.799438   73.990845   138.629578  0.390434    0.0
10   527.972778  371.210907   10.417969    31.941589  0.385738    0.0
11   956.745789  319.386902   11.761597    24.552277  0.381997    0.0
12   742.699341  340.571045    9.885437    25.210480  0.325300    0.0
13   415.444702  412.434967   14.028442    36.732788  0.316447    0.0
14   692.042175  332.803802   11.818115    30.942322  0.308820    0.0
15   600.605225  285.604980    8.357971    20.970306  0.238000    9.0
16  1703.069824  649.187378   78.077271   104.185608  0.227472    0.0
17  1777.966064  610.168823   52.442505    91.897583  0.214207    0.0
18  1828.611328  630.927002   47.544678    90.606750  0.211516    0.0
19   984.745972  332.606079   26.558838    41.515198  0.207436    0.0
20   569.484497  346.354980   12.116089    28.495483  0.205302    0.0
21   807.627014  326.712769    9.158691    16.909149  0.182252    0.0
22   715.789185  334.858063   11.805420    22.541382  0.175251    0.0
23  1725.585938  677.930176   70.131104   101.957092  0.158464    0.0
24   697.893005  333.006226   28.073486    28.604126  0.099073    0.0
25  1740.136963  630.464233   43.717529    74.713501  0.098868    0.0
26   388.227234  392.773193    8.991974    21.113007  0.091106    0.0
27  1794.209229  630.646240   61.527466   106.581116  0.084217    0.0
28  1373.451172  342.751099    9.210571    24.034241  0.083496    0.0
29   649.039368  241.906372   13.403198    23.178558  0.073765    9.0
../../_images/dd6edf2ad014899f477254ba6d653c87af1c5c22de7cb2bf68a86670f9c9f266.png
from soccertrack.logger import tqdm

preds_list = []
for frame in tqdm(cam.iter_frames()):
    det_result = det_model(frame, size = 500)
    
    preds  = det_result.pred
    preds_list.append(preds)
 14%|█▍        | 615/4432 [24:34<2:32:32,  2.40s/it]
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[3], line 5
      3 preds_list = []
      4 for frame in tqdm(cam.iter_frames()):
----> 5     det_result = det_model(frame, size = 500)
      7     preds  = det_result.pred
      8     preds_list.append(preds)

File ~/Github/SoccerTrack/soccertrack/detection_model/base.py:156, in BaseDetectionModel.__call__(self, inputs, **kwargs)
    154 def __call__(self, inputs, **kwargs):
    155     inputs = self._check_and_fix_inputs(inputs)
--> 156     results = self.forward(inputs, **kwargs)
    157     results = self._check_and_fix_outputs(results)
    158     detections = self._postprocess(results, inputs)

File ~/Github/SoccerTrack/soccertrack/detection_model/yolov5.py:29, in YOLOv5.forward(self, x, **kwargs)
     19         return {}
     20     return {
     21         "bbox_left": r[:, 0] - r[:, 2] / 2,
     22         "bbox_top": r[:, 1] - r[:, 3] / 2,
   (...)
     26         "class": r[:, 5],
     27     }
---> 29 results = self.model(x, **kwargs).xywh
     30 results = [to_dict(r) for r in results]
     32 return results

File ~/miniconda3/envs/soccertrack-gui/lib/python3.10/site-packages/torch/nn/modules/module.py:1194, in Module._call_impl(self, *input, **kwargs)
   1190 # If we don't have any hooks, we want to skip the rest of the logic in
   1191 # this function, and just call forward.
   1192 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1193         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1194     return forward_call(*input, **kwargs)
   1195 # Do not call functions when jit is used
   1196 full_backward_hooks, non_full_backward_hooks = [], []

File ~/miniconda3/envs/soccertrack-gui/lib/python3.10/site-packages/torch/autograd/grad_mode.py:27, in _DecoratorContextManager.__call__.<locals>.decorate_context(*args, **kwargs)
     24 @functools.wraps(func)
     25 def decorate_context(*args, **kwargs):
     26     with self.clone():
---> 27         return func(*args, **kwargs)

File ~/Github/SoccerTrack/external/yolov5/models/common.py:708, in AutoShape.forward(self, ims, size, augment, profile)
    705 with amp.autocast(autocast):
    706     # Inference
    707     with dt[1]:
--> 708         y = self.model(x, augment=augment)  # forward
    710     # Post-process
    711     with dt[2]:

File ~/miniconda3/envs/soccertrack-gui/lib/python3.10/site-packages/torch/nn/modules/module.py:1194, in Module._call_impl(self, *input, **kwargs)
   1190 # If we don't have any hooks, we want to skip the rest of the logic in
   1191 # this function, and just call forward.
   1192 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1193         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1194     return forward_call(*input, **kwargs)
   1195 # Do not call functions when jit is used
   1196 full_backward_hooks, non_full_backward_hooks = [], []

File ~/Github/SoccerTrack/external/yolov5/models/common.py:518, in DetectMultiBackend.forward(self, im, augment, visualize)
    515     im = im.permute(0, 2, 3, 1)  # torch BCHW to numpy BHWC shape(1,320,192,3)
    517 if self.pt:  # PyTorch
--> 518     y = self.model(im, augment=augment, visualize=visualize) if augment or visualize else self.model(im)
    519 elif self.jit:  # TorchScript
    520     y = self.model(im)

File ~/miniconda3/envs/soccertrack-gui/lib/python3.10/site-packages/torch/nn/modules/module.py:1194, in Module._call_impl(self, *input, **kwargs)
   1190 # If we don't have any hooks, we want to skip the rest of the logic in
   1191 # this function, and just call forward.
   1192 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1193         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1194     return forward_call(*input, **kwargs)
   1195 # Do not call functions when jit is used
   1196 full_backward_hooks, non_full_backward_hooks = [], []

File ~/Github/SoccerTrack/external/yolov5/models/yolo.py:211, in DetectionModel.forward(self, x, augment, profile, visualize)
    209 if augment:
    210     return self._forward_augment(x)  # augmented inference, None
--> 211 return self._forward_once(x, profile, visualize)

File ~/Github/SoccerTrack/external/yolov5/models/yolo.py:123, in BaseModel._forward_once(self, x, profile, visualize)
    121 if profile:
    122     self._profile_one_layer(m, x, dt)
--> 123 x = m(x)  # run
    124 y.append(x if m.i in self.save else None)  # save output
    125 if visualize:

File ~/miniconda3/envs/soccertrack-gui/lib/python3.10/site-packages/torch/nn/modules/module.py:1194, in Module._call_impl(self, *input, **kwargs)
   1190 # If we don't have any hooks, we want to skip the rest of the logic in
   1191 # this function, and just call forward.
   1192 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1193         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1194     return forward_call(*input, **kwargs)
   1195 # Do not call functions when jit is used
   1196 full_backward_hooks, non_full_backward_hooks = [], []

File ~/Github/SoccerTrack/external/yolov5/models/common.py:169, in C3.forward(self, x)
    168 def forward(self, x):
--> 169     return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1))

File ~/miniconda3/envs/soccertrack-gui/lib/python3.10/site-packages/torch/nn/modules/module.py:1194, in Module._call_impl(self, *input, **kwargs)
   1190 # If we don't have any hooks, we want to skip the rest of the logic in
   1191 # this function, and just call forward.
   1192 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1193         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1194     return forward_call(*input, **kwargs)
   1195 # Do not call functions when jit is used
   1196 full_backward_hooks, non_full_backward_hooks = [], []

File ~/miniconda3/envs/soccertrack-gui/lib/python3.10/site-packages/torch/nn/modules/container.py:204, in Sequential.forward(self, input)
    202 def forward(self, input):
    203     for module in self:
--> 204         input = module(input)
    205     return input

File ~/miniconda3/envs/soccertrack-gui/lib/python3.10/site-packages/torch/nn/modules/module.py:1194, in Module._call_impl(self, *input, **kwargs)
   1190 # If we don't have any hooks, we want to skip the rest of the logic in
   1191 # this function, and just call forward.
   1192 if not (self._backward_hooks or self._forward_hooks or self._forward_pre_hooks or _global_backward_hooks
   1193         or _global_forward_hooks or _global_forward_pre_hooks):
-> 1194     return forward_call(*input, **kwargs)
   1195 # Do not call functions when jit is used
   1196 full_backward_hooks, non_full_backward_hooks = [], []

File ~/Github/SoccerTrack/external/yolov5/models/common.py:122, in Bottleneck.forward(self, x)
    121 def forward(self, x):
--> 122     return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x))

KeyboardInterrupt: 
import numpy as np
import cv2
from soccertrack.utils import cv2pil, make_video
from scipy.signal import savgol_filter

def plot_frame(preds_list, camera):
    frame = camera.get_frame(0).copy()
    
    frame_xmin, frame_ymin = 0, 100
    frame_xmax, frame_ymax = 1920, 800

    frame_x = frame.shape[1]
    frame_y = frame.shape[0]
    
    new_frame_width = 1000#int(frame_x // 1.8)
    new_frame_height = 500#int(frame_y // 1.8)

    bxs, bys = [], []
    for preds in preds_list:
        preds = preds[(preds[:, 0] + preds[:, 2] < frame_xmax) & (preds[:, 1] + preds[:, 3] < frame_ymax)]
    
        new_frame_bx = preds[:, 0].mean() - new_frame_width // 2
        new_frame_by = preds[:, 1].mean() - new_frame_height // 2
        
        # center the box on the ball but keep it within the frame
        bx = int(min(max(new_frame_bx / 2, 0), frame_xmax - new_frame_width))
        by = int(min(max(new_frame_by/ 2, 200), (frame_ymax - new_frame_height)))
        bxs.append(bx)
        bys.append(by)
    
    bxs = savgol_filter(bxs, 501, 3).astype(int)
    bys = savgol_filter(bys, 501, 3).astype(int)
    
    for (bx, by), _frame in zip(zip(bxs, bys), camera.iter_frames()):
        frame = _frame.copy()
        
        # Mask out the box but keep the background visible with a bit of transparency
        opacity = 0.5
        mask = np.zeros(frame.shape, dtype=np.uint8)
        mask = cv2.rectangle(mask, (bx, by), (bx + new_frame_width, by + new_frame_height), (255, 255, 255), -1)

        masked_frame = cv2.bitwise_and(frame, mask)
        masked_frame = cv2.addWeighted(masked_frame, opacity, frame, 1 - opacity, 0)
        
        # crop the frame to the box
        masked_frame = masked_frame[by:by + new_frame_height, bx:bx + new_frame_width]
        
        yield masked_frame

make_video(plot_frame(preds_list, cam), 'test.mp4')
Writing video: 615it [00:40, 15.28it/s]
from IPython.display import Video

Video('test.mp4')
import matplotlib.font_manager as fm
fm.fontManager.addfont('/Users/atom/Downloads/xkcd-Regular-webfont/xkcd-Regular-webfont.ttf')
codf.visualize_frame(1, home_key='0', away_key='1', ball_key='3', 
    home_kwargs={'markerfacecolor': 'white'},
    away_kwargs={'markerfacecolor': 'blue'})
# Importing the Seaborn library
import seaborn as sns
import matplotlib.pyplot as plt

data = pd.read_csv('/Users/atom/Downloads/readthedocs_traffic_analytics_soccertrack_2023-05-13_2023-08-11.csv')

# Converting the 'Date' column to a datetime object
data['Date'] = pd.to_datetime(data['Date'])

# Resampling the data by month and summing the views
monthly_views = data.resample('M', on='Date')['Views'].sum().reset_index()

# Setting the xkcd style
plt.xkcd()

# Creating the bar plot
plt.figure(figsize=(10, 6), facecolor='#0C1118')
sns.barplot(x=monthly_views['Date'].dt.strftime('%B %Y'), y=monthly_views['Views'], palette="Blues_d")

# Adding labels and title
plt.title('Monthly Views (Read the Docs - SoccerTrack Project)')
plt.xlabel('Month')
plt.ylabel('Number of Views')
plt.grid(True, color="white", linestyle="--", linewidth=0.5)
plt.xticks(rotation=45)

# Displaying the plot
plt.show()
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
findfont: Font family 'xkcd Script' not found.
findfont: Font family 'Humor Sans' not found.
findfont: Font family 'Comic Neue' not found.
../../_images/0f33a7485e0c29dbb91ae690a9ba0396de44d727656ba89a825b739cbbafcee1.png