Spaces:
Running
Running
| # app.py | |
| import streamlit as st | |
| import cv2 | |
| import numpy as np | |
| import pandas as pd | |
| from ultralytics import YOLO | |
| import supervision as sv | |
| from scipy.spatial import distance as dist | |
| import tempfile | |
| import matplotlib.pyplot as plt | |
| # Sidebar with logo and team information | |
| with st.sidebar: | |
| st.image("https://cs.christuniversity.in/softex/resources/img/christ_university_Black.png", width=200) | |
| st.markdown("<h4 style='text-align: center; margin-top: 0;'>Christ University Kengeri Campus</h4>", unsafe_allow_html=True) | |
| st.markdown("<h5 style='text-align: center; margin-top: 0;'>Sports Department</h5>", unsafe_allow_html=True) | |
| st.markdown("### Team Members") | |
| st.markdown(""" | |
| - Harsh Vardhan Lal 2262069 6BTCSAIML B | |
| - Harsheet Sandeep Thakur 2262070 6BTCSAIML B | |
| - Nandalal C B 2262115 6BTCSAIML B | |
| - Athul Nambiar 2262041 6BTCSAIML B | |
| """) | |
| # Initialize components | |
| model = YOLO('yolov8s.pt') | |
| tracker = sv.ByteTrack() | |
| # Streamlit UI | |
| st.title("⚽️ Player Tracking System") | |
| uploaded_video = st.file_uploader("Upload match video", type=["mp4", "mov"]) | |
| calibration_dist = st.number_input("Field width in meters (for speed calibration):", value=68.0) | |
| # Initialize session state | |
| if 'player_data' not in st.session_state: | |
| st.session_state.player_data = {} | |
| # Create a placeholder for the live tracking table | |
| live_table_container = st.container() | |
| live_table = live_table_container.empty() | |
| if uploaded_video: | |
| tfile = tempfile.NamedTemporaryFile(delete=False) | |
| tfile.write(uploaded_video.read()) | |
| cap = cv2.VideoCapture(tfile.name) | |
| fps = cap.get(cv2.CAP_PROP_FPS) | |
| frame_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| pixels_per_meter = frame_width / calibration_dist if calibration_dist > 0 else 1.0 | |
| st_frame = st.empty() | |
| frame_count = 0 | |
| # Clear previous player data | |
| st.session_state.player_data = {} | |
| player_count = 0 # Track the number of distinct players | |
| while cap.isOpened(): | |
| ret, frame = cap.read() | |
| if not ret: | |
| break | |
| # Detection + tracking with person class filter (class 0) | |
| results = model(frame)[0] | |
| detections = sv.Detections.from_ultralytics(results) | |
| # Filter for person class only (class 0 in COCO dataset) | |
| person_mask = np.array([class_id == 0 for class_id in detections.class_id]) | |
| detections = detections[person_mask] | |
| # Apply tracking - this will assign consistent IDs | |
| detections = tracker.update_with_detections(detections) | |
| # Extract bounding boxes, track IDs, etc. | |
| boxes = detections.xyxy # Get bounding boxes [x1, y1, x2, y2] | |
| track_ids = detections.tracker_id # Get track IDs | |
| if boxes is not None and len(boxes) > 0: | |
| for i, (box, track_id) in enumerate(zip(boxes, track_ids)): | |
| # Skip detections without a track_id | |
| if track_id is None: | |
| continue | |
| # Use the track_id directly from ByteTrack | |
| player_id = int(track_id) | |
| # Calculate center point of bounding box | |
| x1, y1, x2, y2 = box | |
| centroid = (int((x1 + x2) / 2), int((y1 + y2) / 2)) | |
| # Initialize speed to 0 for all cases | |
| speed = 0.0 | |
| # Initialize new player if not seen before | |
| if player_id not in st.session_state.player_data: | |
| st.session_state.player_data[player_id] = { | |
| 'positions': [centroid], | |
| 'timestamps': [frame_count / fps], | |
| 'distance': 0.0, | |
| 'speeds': [0.0], # Initialize with zero speed | |
| 'last_seen': frame_count | |
| } | |
| player_count += 1 # Increment player counter | |
| else: | |
| # Calculate movement metrics | |
| prev_pos = st.session_state.player_data[player_id]['positions'][-1] | |
| time_diff = (frame_count / fps) - st.session_state.player_data[player_id]['timestamps'][-1] | |
| # Calculate distance | |
| pixel_dist = dist.euclidean(prev_pos, centroid) | |
| # Apply motion smoothing to ignore unrealistic movements | |
| if pixel_dist < frame_width * 0.1: # Max 10% of screen width per frame | |
| speed_px = pixel_dist / time_diff if time_diff > 0 else 0.0 | |
| # Convert to meters per second | |
| speed = speed_px / pixels_per_meter | |
| meter_dist = pixel_dist / pixels_per_meter | |
| # Update player data | |
| st.session_state.player_data[player_id]['positions'].append(centroid) | |
| st.session_state.player_data[player_id]['timestamps'].append(frame_count / fps) | |
| st.session_state.player_data[player_id]['distance'] += meter_dist | |
| st.session_state.player_data[player_id]['speeds'].append(speed) | |
| st.session_state.player_data[player_id]['last_seen'] = frame_count | |
| else: | |
| # Just update position without adding to distance/speed | |
| st.session_state.player_data[player_id]['positions'].append(centroid) | |
| st.session_state.player_data[player_id]['timestamps'].append(frame_count / fps) | |
| st.session_state.player_data[player_id]['last_seen'] = frame_count | |
| # Maintain previous speed | |
| speed = st.session_state.player_data[player_id]['speeds'][-1] if st.session_state.player_data[player_id]['speeds'] else 0 | |
| # Draw player info on frame | |
| label = f"ID:{player_id} Speed:{speed:.1f}m/s" | |
| cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) | |
| cv2.putText(frame, label, (int(x1), int(y1-10)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) | |
| # Update live table | |
| if st.session_state.player_data: | |
| live_data = [] | |
| for player_id, data in st.session_state.player_data.items(): | |
| # Only show recently seen players | |
| if frame_count - data['last_seen'] < 30: | |
| current_speed = data['speeds'][-1] if data['speeds'] else 0 | |
| live_data.append({ | |
| "Player": f"Player {player_id}", | |
| "Current Speed (m/s)": round(current_speed, 2), | |
| "Distance (m)": round(data['distance'], 2), | |
| "Time (s)": round(data['timestamps'][-1], 2) if data['timestamps'] else 0 | |
| }) | |
| live_df = pd.DataFrame(live_data) | |
| if not live_df.empty: | |
| live_table.dataframe(live_df, use_container_width=True, hide_index=True) | |
| st_frame.image(frame, channels="BGR") | |
| frame_count += 1 | |
| cap.release() | |
| # Display final analytics | |
| st.subheader(f"Player Statistics ({player_count} players detected)") | |
| # Create player stats table | |
| stats_data = [] | |
| for player_id, data in st.session_state.player_data.items(): | |
| if len(data['speeds']) > 0: | |
| avg_speed = np.mean(data['speeds']) | |
| max_speed = np.max(data['speeds']) | |
| else: | |
| avg_speed = 0 | |
| max_speed = 0 | |
| duration = data['timestamps'][-1] - data['timestamps'][0] if len(data['timestamps']) > 1 else 0 | |
| # Only include players with significant tracking data | |
| if len(data['positions']) > 10: | |
| stats_data.append({ | |
| "Player": f"Player {player_id}", | |
| "Total Distance (m)": round(data['distance'], 2), | |
| "Avg Speed (m/s)": round(avg_speed, 2), | |
| "Max Speed (m/s)": round(max_speed, 2), | |
| "Duration (s)": round(duration, 2) | |
| }) | |
| stats_df = pd.DataFrame(stats_data) | |
| st.dataframe(stats_df, use_container_width=True, hide_index=True) | |
| # Create speed comparison chart | |
| if st.session_state.player_data: | |
| st.subheader("Player Speed Comparison") | |
| fig, ax = plt.subplots(figsize=(10, 6)) | |
| for player_id, data in st.session_state.player_data.items(): | |
| # Only plot players with significant data | |
| if len(data['speeds']) > 10: | |
| ax.plot(data['timestamps'], data['speeds'], label=f"Player {player_id}") | |
| ax.set_xlabel('Time (seconds)') | |
| ax.set_ylabel('Speed (m/s)') | |
| ax.set_title('Player Speed Over Time') | |
| ax.legend() | |
| ax.grid(True) | |
| st.pyplot(fig) | |