Spaces:
Running
Running
from datetime import datetime | |
import json | |
import os | |
import streamlit as st | |
import requests | |
import pandas as pd | |
from io import StringIO | |
import plotly.graph_objects as go | |
# Top down page rendering | |
st.set_page_config(page_title='Hockey Breeds v3 - Pressure Meter', layout="wide", | |
page_icon=":frame_with_picture:") | |
st.title('Hockey Breeds v3 - Pressure Meter') | |
intro = '''Version 3 of Hockey Breeds introduces a new feature: the **Pressure Meter**. Pressure is a term used in hockey to describe the buildup of offensive momuntum which often leads to goals. | |
The **Pressure Meter** builds on a number of major enhancements to the Top-Shelf AI platform: | |
1. Improved and expanded data set and improved model | |
1. Parallelized processing pipeline for processing input video and generating output metrics in *real time* | |
1. Analysis and metrics include: | |
* Team jersey color determination | |
* Player team assignments | |
* Skater speeds and accelerations | |
* Player positions relative to nearest goalie & net | |
* Improved puck tracking and interpolation | |
* Game play state analysis (stoppage vs live play) | |
''' | |
st.markdown(intro) | |
st.subheader('Pressure Meter Visualization') | |
# get the data file location | |
data_location = st.text_input('Enter the location of the stream analytics metadata file', | |
value='https://storage.googleapis.com/topshelf-clients/pressure-meter/2025-02-09/22809/stream_metadata.json') | |
metadata = None | |
stream_base_url = None | |
if data_location: | |
# should be an http link | |
if not data_location.startswith('http'): | |
st.error('Data location must be an http link') | |
else: | |
# download the data from the link | |
if data_location.endswith('/'): | |
data_location = data_location + 'stream_metadata.json' | |
data = requests.get(data_location) | |
# load the data from the json file | |
metadata = json.loads(data.text) | |
# determine the base url for the stream | |
stream_base_url = data_location.split('stream_metadata.json')[0] | |
# load the data from the csv files | |
if metadata: | |
# get the data from the csv files | |
files = metadata['output_files'] | |
# get the base timestamp for the stream | |
base_timestamp = datetime.fromisoformat(metadata['video_start_time']) | |
# Create an empty list to store individual dataframes | |
dfs = [] | |
for ts, file in files.items(): | |
try: | |
response = requests.get(stream_base_url + file) | |
response.raise_for_status() | |
data_string = StringIO(response.text) | |
df = pd.read_csv(data_string) | |
ts_delta = datetime.fromtimestamp(int(ts)).astimezone(base_timestamp.tzinfo) - base_timestamp | |
df['second_offset'] = df['second_offset'] + ts_delta.total_seconds() | |
dfs.append(df) | |
except Exception as e: | |
st.error(f"Failed to load data for timestamp {ts}, file: {file}") | |
st.error(f"Error: {str(e)}") | |
continue | |
# Log the number of files processed | |
st.info(f"Successfully loaded {len(dfs)} out of {len(files)} files") | |
# Concatenate all dataframes and sort by the second_offset | |
combined_df = pd.concat(dfs, ignore_index=True) | |
combined_df = combined_df.sort_values('second_offset') | |
# Check for gaps in the sequence | |
expected_range = set(range(int(combined_df['second_offset'].min()), | |
int(combined_df['second_offset'].max()) + 1)) | |
actual_range = set(combined_df['second_offset'].astype(int)) | |
missing_seconds = sorted(expected_range - actual_range) | |
if missing_seconds: | |
st.warning("Found gaps in the data sequence:") | |
# Group consecutive missing seconds into ranges for cleaner output | |
gaps = [] | |
start = missing_seconds[0] | |
prev = start | |
for curr in missing_seconds[1:] + [None]: | |
if curr != prev + 1: | |
if start == prev: | |
gaps.append(f"{start}") | |
else: | |
gaps.append(f"{start}-{prev}") | |
start = curr | |
prev = curr | |
st.warning(f"Missing seconds: {', '.join(gaps)}") | |
# Calculate cumulative counts and ratios - only count actual pressure values | |
combined_df['team1_cumulative'] = (combined_df['pressure_balance'] > 0).astype(int).cumsum() | |
combined_df['team2_cumulative'] = (combined_df['pressure_balance'] < 0).astype(int).cumsum() | |
combined_df['total_cumulative'] = combined_df['team1_cumulative'] + combined_df['team2_cumulative'] | |
# Avoid division by zero by using where | |
combined_df['team1_pressure_ratio'] = (combined_df['team1_cumulative'] / | |
combined_df['total_cumulative'].where(combined_df['total_cumulative'] > 0, 1)) | |
combined_df['team2_pressure_ratio'] = (combined_df['team2_cumulative'] / | |
combined_df['total_cumulative'].where(combined_df['total_cumulative'] > 0, 1)) | |
# Calculate the ratio difference for the balance visualization | |
combined_df['pressure_ratio_diff'] = combined_df['team1_pressure_ratio'] - combined_df['team2_pressure_ratio'] | |
# Add pressure balance visualization using the ratio difference | |
st.subheader("Pressure Waves") | |
balance_df = pd.DataFrame({ | |
'second_offset': combined_df['second_offset'], | |
'pressure_ratio_diff': combined_df['pressure_ratio_diff'] | |
}) | |
# Get team colors from metadata and parse them | |
def parse_rgb(color_str): | |
# Extract numbers from format 'rgb(r,g,b)' | |
r, g, b = map(int, color_str.strip('rgb()').split(',')) | |
return r, g, b | |
team1_color = metadata.get('team1_color', 'rgb(54, 162, 235)') # default blue if not found | |
team2_color = metadata.get('team2_color', 'rgb(255, 99, 132)') # default red if not found | |
# Parse RGB values | |
team1_rgb = parse_rgb(team1_color) | |
team2_rgb = parse_rgb(team2_color) | |
fig = go.Figure() | |
# Add positive values with team1 color | |
fig.add_trace( | |
go.Scatter( | |
x=combined_df['second_offset'], | |
y=combined_df['pressure_ratio_diff'].clip(lower=0), | |
fill='tozeroy', | |
fillcolor=f'rgba{(*team1_rgb, 0.2)}', | |
line=dict( | |
color=team1_color, | |
shape='hv' | |
), | |
name='Team 1 Dominant', | |
hovertemplate='Time: %{x:.1f}s<br>Dominance: %{y:.2f}<br>Team 1<extra></extra>', | |
hoveron='points+fills' | |
) | |
) | |
# Add negative values with team2 color | |
fig.add_trace( | |
go.Scatter( | |
x=combined_df['second_offset'], | |
y=combined_df['pressure_ratio_diff'].clip(upper=0), | |
fill='tozeroy', | |
fillcolor=f'rgba{(*team2_rgb, 0.2)}', | |
line=dict( | |
color=team2_color, | |
shape='hv' | |
), | |
name='Team 2 Dominant', | |
hovertemplate='Time: %{x:.1f}s<br>Dominance: %{y:.2f}<br>Team 2<extra></extra>', | |
hoveron='points+fills' | |
) | |
) | |
fig.update_layout( | |
yaxis=dict( | |
range=[-1, 1], | |
zeroline=True, | |
zerolinewidth=2, | |
zerolinecolor='rgba(0,0,0,0.2)', | |
gridcolor='rgba(0,0,0,0.1)', | |
title='Team Dominance' | |
), | |
xaxis=dict( | |
title='Time (seconds)', | |
gridcolor='rgba(0,0,0,0.1)' | |
), | |
plot_bgcolor='white', | |
height=400, | |
margin=dict(l=0, r=0, t=20, b=0), | |
showlegend=True, | |
legend=dict( | |
yanchor="top", | |
y=0.99, | |
xanchor="left", | |
x=0.01 | |
) | |
) | |
st.plotly_chart(fig, use_container_width=True) | |
with st.expander("Pressure Data"): | |
st.write(combined_df) | |
# add details in a sub section with expander | |
with st.expander("Pressure Meter Details"): | |
st.write(""" | |
The Pressure Meter is a visualization of the pressure waves in the game. It is a line chart of the cumulative pressure counts for each team over time. | |
""") | |
# Create two columns for charts | |
col1, col2 = st.columns(2) | |
with col1: | |
st.subheader("Cumulative Pressure Counts") | |
st.line_chart(combined_df, x='second_offset', y=['team1_cumulative', 'team2_cumulative']) | |
with col2: | |
st.subheader("Pressure Ratio Over Time") | |
st.area_chart(combined_df, | |
x='second_offset', | |
y=['team1_pressure_ratio', 'team2_pressure_ratio']) | |
# Show current dominance percentage | |
current_ratio = combined_df.iloc[-1]['pressure_balance'] | |
if current_ratio > 0: | |
dominant_team = 'Team 1' | |
pressure_value = current_ratio | |
elif current_ratio < 0: | |
dominant_team = 'Team 2' | |
pressure_value = abs(current_ratio) | |
else: | |
dominant_team = 'Neutral' | |
pressure_value = 0 | |
st.metric( | |
label="Dominant Team Pressure", | |
value=f"{dominant_team}", | |
delta=f"{pressure_value*100:.1f}%" | |
) | |
# After loading metadata | |
st.subheader("Data Files Analysis") | |
# Analyze the timestamps in the metadata | |
timestamps = sorted([int(ts) for ts in files.keys()]) | |
time_diffs = [timestamps[i+1] - timestamps[i] for i in range(len(timestamps)-1)] | |
st.info(f"Number of data files: {len(files)}") | |
st.info(f"Time range: {datetime.fromtimestamp(timestamps[0])} to {datetime.fromtimestamp(timestamps[-1])}") | |
st.info(f"Time differences between files: {set(time_diffs)} seconds") | |
# Show the actual files and timestamps | |
with st.expander("Stream Metadata Details"): | |
st.write(metadata) | |
# Log the data range | |
st.write(f"Data range: {combined_df['second_offset'].min():.1f}s to {combined_df['second_offset'].max():.1f}s") | |
st.write(f"Total rows: {len(combined_df)}") | |
for ts in sorted(files.keys()): | |
st.text(f"Timestamp: {datetime.fromtimestamp(int(ts))} - File: {files[ts]}") |