- Organized project into src directory with subpackages (analysis, data, visualization, utils) - Added comprehensive README with project overview and structure - Implemented data loading, bounce detection, and visualization modules - Created example scripts and Jupyter notebook for project usage - Added requirements.txt for dependency management - Included output files for different ball types (golf, lacrosse, metal)
287 lines
9.1 KiB
Python
287 lines
9.1 KiB
Python
"""
|
|
Animation module for visualizing bouncing ball data.
|
|
"""
|
|
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
import matplotlib.animation as animation
|
|
from matplotlib.patches import Circle
|
|
import pandas as pd
|
|
from IPython.display import HTML
|
|
|
|
|
|
def create_ball_animation(df, ball_radius=0.5, fps=30, duration=None, title="Bouncing Ball Animation"):
|
|
"""
|
|
Create an animation of a bouncing ball from position data.
|
|
|
|
Parameters:
|
|
df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns
|
|
ball_radius (float): Radius of the ball in inches
|
|
fps (int): Frames per second for the animation
|
|
duration (float): Duration of the animation in seconds (if None, uses the full data)
|
|
title (str): Title for the animation
|
|
|
|
Returns:
|
|
matplotlib.animation.Animation: The animation object
|
|
"""
|
|
# Extract time and position data
|
|
time_data = df['Time'].values
|
|
position_data = df['Position'].values
|
|
|
|
# Determine animation duration
|
|
if duration is None:
|
|
duration = time_data[-1]
|
|
|
|
# Calculate the number of frames based on fps and duration
|
|
num_frames = int(fps * duration)
|
|
|
|
# Create figure and axis
|
|
fig, ax = plt.subplots(figsize=(10, 6))
|
|
|
|
# Set axis limits
|
|
max_height = np.max(position_data) + 2 * ball_radius
|
|
ax.set_xlim(-5, 5)
|
|
ax.set_ylim(0, max_height)
|
|
|
|
# Set labels and title
|
|
ax.set_xlabel('X Position (inches)')
|
|
ax.set_ylabel('Y Position (inches)')
|
|
ax.set_title(title)
|
|
|
|
# Add a horizontal line at y=0 to represent the ground
|
|
ax.axhline(y=0, color='black', linestyle='-', linewidth=2)
|
|
|
|
# Create the ball (circle patch)
|
|
ball = Circle((0, position_data[0]), ball_radius, color='red', zorder=2)
|
|
ax.add_patch(ball)
|
|
|
|
# Create a line to show the ball's path
|
|
line, = ax.plot([], [], 'b-', alpha=0.3, linewidth=1)
|
|
|
|
# Initialize empty lists for the path
|
|
path_x = []
|
|
path_y = []
|
|
|
|
def init():
|
|
"""Initialize the animation"""
|
|
ball.center = (0, position_data[0])
|
|
line.set_data([], [])
|
|
return ball, line
|
|
|
|
def animate(i):
|
|
"""Update the animation for frame i"""
|
|
# Calculate the current time based on the frame number
|
|
current_time = i * duration / num_frames
|
|
|
|
# Find the closest time index in our data
|
|
idx = np.abs(time_data - current_time).argmin()
|
|
|
|
# Update ball position
|
|
y_pos = position_data[idx]
|
|
ball.center = (0, y_pos)
|
|
|
|
# Update path
|
|
path_x.append(0)
|
|
path_y.append(y_pos)
|
|
line.set_data(path_x, path_y)
|
|
|
|
return ball, line
|
|
|
|
# Create the animation
|
|
anim = animation.FuncAnimation(
|
|
fig, animate, init_func=init, frames=num_frames,
|
|
interval=1000/fps, blit=True
|
|
)
|
|
|
|
plt.close() # Prevent the static plot from displaying
|
|
|
|
return anim
|
|
|
|
|
|
def create_ball_animation_with_model(df, peak_indices, cor, initial_height,
|
|
ball_radius=0.5, fps=30, duration=None,
|
|
title="Bouncing Ball Animation with Model",
|
|
g=386.09):
|
|
"""
|
|
Create an animation of a bouncing ball from position data with an ideal model comparison.
|
|
|
|
Parameters:
|
|
df (pandas.DataFrame): DataFrame with 'Time' and 'Position' columns
|
|
peak_indices (numpy.ndarray): Indices of detected bounces
|
|
cor (float): Coefficient of Restitution
|
|
initial_height (float): Initial height of the ball in inches
|
|
ball_radius (float): Radius of the ball in inches
|
|
fps (int): Frames per second for the animation
|
|
duration (float): Duration of the animation in seconds (if None, uses the full data)
|
|
title (str): Title for the animation
|
|
g (float): Acceleration due to gravity (in inches/s²)
|
|
|
|
Returns:
|
|
matplotlib.animation.Animation: The animation object
|
|
"""
|
|
# Extract time and position data
|
|
time_data = df['Time'].values
|
|
position_data = df['Position'].values
|
|
|
|
# Determine animation duration
|
|
if duration is None:
|
|
duration = time_data[-1]
|
|
|
|
# Calculate the number of frames based on fps and duration
|
|
num_frames = int(fps * duration)
|
|
|
|
# Create figure and axis
|
|
fig, ax = plt.subplots(figsize=(10, 6))
|
|
|
|
# Set axis limits
|
|
max_height = np.max(position_data) + 2 * ball_radius
|
|
ax.set_xlim(-5, 5)
|
|
ax.set_ylim(0, max_height)
|
|
|
|
# Set labels and title
|
|
ax.set_xlabel('X Position (inches)')
|
|
ax.set_ylabel('Y Position (inches)')
|
|
ax.set_title(title)
|
|
|
|
# Add a horizontal line at y=0 to represent the ground
|
|
ax.axhline(y=0, color='black', linestyle='-', linewidth=2)
|
|
|
|
# Create the real data ball (circle patch)
|
|
real_ball = Circle((0, position_data[0]), ball_radius, color='red', zorder=2, label='Actual')
|
|
ax.add_patch(real_ball)
|
|
|
|
# Create the model ball (circle patch)
|
|
model_ball = Circle((2, initial_height), ball_radius, color='blue', zorder=2, alpha=0.7, label='Model')
|
|
ax.add_patch(model_ball)
|
|
|
|
# Create lines to show the balls' paths
|
|
real_line, = ax.plot([], [], 'r-', alpha=0.3, linewidth=1)
|
|
model_line, = ax.plot([], [], 'b-', alpha=0.3, linewidth=1)
|
|
|
|
# Add legend
|
|
ax.legend(handles=[
|
|
plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='red', markersize=10, label='Actual'),
|
|
plt.Line2D([0], [0], marker='o', color='w', markerfacecolor='blue', markersize=10, label='Model')
|
|
])
|
|
|
|
# Initialize empty lists for the paths
|
|
real_path_x = []
|
|
real_path_y = []
|
|
model_path_x = []
|
|
model_path_y = []
|
|
|
|
# Generate model data
|
|
dt = 0.001 # Time step for simulation
|
|
model_time = []
|
|
model_position = []
|
|
|
|
# Initialize simulation parameters
|
|
y = initial_height
|
|
v_y = 0.0 # Starting from rest
|
|
t = 0.0
|
|
|
|
# Run simulation for the duration
|
|
while t <= duration:
|
|
model_time.append(t)
|
|
model_position.append(y)
|
|
|
|
# Kinematic update
|
|
v_y -= g * dt
|
|
y += v_y * dt
|
|
t += dt
|
|
|
|
# Bounce condition
|
|
if y <= 0:
|
|
y = 0 # Ground impact
|
|
v_y = -v_y * cor # Apply COR
|
|
|
|
# Convert to numpy arrays for faster indexing
|
|
model_time = np.array(model_time)
|
|
model_position = np.array(model_position)
|
|
|
|
def init():
|
|
"""Initialize the animation"""
|
|
real_ball.center = (0, position_data[0])
|
|
model_ball.center = (2, initial_height)
|
|
real_line.set_data([], [])
|
|
model_line.set_data([], [])
|
|
return real_ball, model_ball, real_line, model_line
|
|
|
|
def animate(i):
|
|
"""Update the animation for frame i"""
|
|
# Calculate the current time based on the frame number
|
|
current_time = i * duration / num_frames
|
|
|
|
# Find the closest time index in our data
|
|
real_idx = np.abs(time_data - current_time).argmin()
|
|
model_idx = np.abs(model_time - current_time).argmin()
|
|
|
|
# Update ball positions
|
|
real_y_pos = position_data[real_idx]
|
|
model_y_pos = model_position[model_idx]
|
|
|
|
real_ball.center = (0, real_y_pos)
|
|
model_ball.center = (2, model_y_pos)
|
|
|
|
# Update paths
|
|
real_path_x.append(0)
|
|
real_path_y.append(real_y_pos)
|
|
model_path_x.append(2)
|
|
model_path_y.append(model_y_pos)
|
|
|
|
real_line.set_data(real_path_x, real_path_y)
|
|
model_line.set_data(model_path_x, model_path_y)
|
|
|
|
return real_ball, model_ball, real_line, model_line
|
|
|
|
# Create the animation
|
|
anim = animation.FuncAnimation(
|
|
fig, animate, init_func=init, frames=num_frames,
|
|
interval=1000/fps, blit=True
|
|
)
|
|
|
|
plt.close() # Prevent the static plot from displaying
|
|
|
|
return anim
|
|
|
|
|
|
def display_animation(anim, html_video=True):
|
|
"""
|
|
Display an animation in a Jupyter notebook.
|
|
|
|
Parameters:
|
|
anim (matplotlib.animation.Animation): The animation to display
|
|
html_video (bool): If True, convert to HTML5 video, otherwise use JavaScript
|
|
|
|
Returns:
|
|
IPython.display.HTML: The HTML object containing the animation
|
|
"""
|
|
if html_video:
|
|
# Convert to HTML5 video
|
|
html = anim.to_html5_video()
|
|
return HTML(html)
|
|
else:
|
|
# Use JavaScript animation
|
|
return HTML(anim.to_jshtml())
|
|
|
|
|
|
def save_animation(anim, filename, fps=30, dpi=100):
|
|
"""
|
|
Save an animation to a file.
|
|
|
|
Parameters:
|
|
anim (matplotlib.animation.Animation): The animation to save
|
|
filename (str): The filename to save to (should end with .mp4, .gif, etc.)
|
|
fps (int): Frames per second
|
|
dpi (int): Resolution in dots per inch
|
|
"""
|
|
# Determine the writer based on the file extension
|
|
if filename.endswith('.mp4'):
|
|
writer = animation.FFMpegWriter(fps=fps)
|
|
elif filename.endswith('.gif'):
|
|
writer = animation.PillowWriter(fps=fps)
|
|
else:
|
|
raise ValueError("Unsupported file format. Use .mp4 or .gif")
|
|
|
|
# Save the animation
|
|
anim.save(filename, writer=writer, dpi=dpi) |