track visualization for speed, corner and gears
Browse files- app.py +35 -41
- tools.py +30 -2
- utils/track_utils.py +212 -0
app.py
CHANGED
@@ -13,53 +13,26 @@ from utils.constants import (
|
|
13 |
# Variables
|
14 |
CURRENT_YEAR = datetime.datetime.now().year
|
15 |
|
16 |
-
def driver_championship_score(driver_name: str) -> str:
|
17 |
-
"""
|
18 |
-
Get the championship score for the given driver.
|
19 |
-
|
20 |
-
Args:
|
21 |
-
driver_name (str): The driver's name
|
22 |
-
|
23 |
-
Returns:
|
24 |
-
int: The driver's championship score
|
25 |
-
"""
|
26 |
-
return f"Driver {driver_name} has {random.randint(0, 100)} championship points"
|
27 |
-
|
28 |
-
|
29 |
-
def driver_position(driver_name: str) -> str:
|
30 |
-
"""
|
31 |
-
Get the current position of the given driver.
|
32 |
-
|
33 |
-
Args:
|
34 |
-
driver_name (str): The driver's name
|
35 |
-
|
36 |
-
Returns:
|
37 |
-
str: The driver's current position
|
38 |
-
"""
|
39 |
-
return f"Driver {driver_name} is in position {random.randint(1, 20)}"
|
40 |
-
|
41 |
|
42 |
# Load in driver names
|
43 |
with open("assets/driver_names.json") as f:
|
44 |
driver_names = json.load(f)["drivers"]
|
|
|
|
|
45 |
|
46 |
-
iface1 = gr.Interface(
|
47 |
-
fn=driver_championship_score,
|
48 |
-
inputs=gr.Dropdown(driver_names),
|
49 |
-
outputs="text",
|
50 |
-
title="[WIP] Driver Championship Score",
|
51 |
-
description="Get the championship score for the given driver"
|
52 |
-
)
|
53 |
|
54 |
-
|
55 |
-
fn=
|
56 |
-
inputs=
|
|
|
|
|
|
|
57 |
outputs="text",
|
58 |
-
title="
|
59 |
-
description="Get the
|
60 |
)
|
61 |
|
62 |
-
|
63 |
fn=tools.get_event_info,
|
64 |
inputs=[
|
65 |
gr.Number(label="Calendar year", value=CURRENT_YEAR, minimum=1950, maximum=CURRENT_YEAR),
|
@@ -71,7 +44,7 @@ iface3 = gr.Interface(
|
|
71 |
description="Get the Grand Prix event info for a specific race week"
|
72 |
)
|
73 |
|
74 |
-
|
75 |
fn=tools.get_season_calendar,
|
76 |
inputs=[
|
77 |
gr.Number(label="Calendar year", value=CURRENT_YEAR, minimum=1950, maximum=CURRENT_YEAR),
|
@@ -81,9 +54,30 @@ iface4 = gr.Interface(
|
|
81 |
description="Get the season calendar information for the given year"
|
82 |
)
|
83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
|
85 |
-
interface_list = [
|
86 |
-
tab_names = ["
|
87 |
|
88 |
|
89 |
# Combine all the interfaces into a single TabbedInterface
|
|
|
13 |
# Variables
|
14 |
CURRENT_YEAR = datetime.datetime.now().year
|
15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
16 |
|
17 |
# Load in driver names
|
18 |
with open("assets/driver_names.json") as f:
|
19 |
driver_names = json.load(f)["drivers"]
|
20 |
+
with open("assets/constructor_details.json") as f:
|
21 |
+
constructor_names = [constructor["constructor"] for constructor in json.load(f)["constructors"]]
|
22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
23 |
|
24 |
+
iface_driver_championship_standings = gr.Interface(
|
25 |
+
fn=tools.driver_championship_score,
|
26 |
+
inputs=[
|
27 |
+
gr.Number(label="Calendar year", value=CURRENT_YEAR, minimum=1950, maximum=CURRENT_YEAR),
|
28 |
+
gr.Dropdown(driver_names)
|
29 |
+
],
|
30 |
outputs="text",
|
31 |
+
title="Driver Championship Standings",
|
32 |
+
description="Get the championship standings for the given driver"
|
33 |
)
|
34 |
|
35 |
+
iface_event_info = gr.Interface(
|
36 |
fn=tools.get_event_info,
|
37 |
inputs=[
|
38 |
gr.Number(label="Calendar year", value=CURRENT_YEAR, minimum=1950, maximum=CURRENT_YEAR),
|
|
|
44 |
description="Get the Grand Prix event info for a specific race week"
|
45 |
)
|
46 |
|
47 |
+
iface_season_calendar = gr.Interface(
|
48 |
fn=tools.get_season_calendar,
|
49 |
inputs=[
|
50 |
gr.Number(label="Calendar year", value=CURRENT_YEAR, minimum=1950, maximum=CURRENT_YEAR),
|
|
|
54 |
description="Get the season calendar information for the given year"
|
55 |
)
|
56 |
|
57 |
+
iface_track_visualization = gr.Interface(
|
58 |
+
fn=tools.track_visualization,
|
59 |
+
inputs=[
|
60 |
+
gr.Number(label="Calendar year", value=CURRENT_YEAR, minimum=1950, maximum=CURRENT_YEAR),
|
61 |
+
gr.Textbox(label="Grand Prix", placeholder="Ex: Monaco", info="The name of the GP/country/location (Fuzzy matching supported)"),
|
62 |
+
gr.Radio(["speed", "corners", "gear"], label="Visualization type", value="speed"),
|
63 |
+
gr.Dropdown(driver_names)
|
64 |
+
],
|
65 |
+
outputs="image",
|
66 |
+
title="[WIP] Track Visualization",
|
67 |
+
description="Get the track visualization for the given Grand Prix"
|
68 |
+
)
|
69 |
+
|
70 |
+
|
71 |
+
# TODO swap to this format
|
72 |
+
named_interfaces = {
|
73 |
+
"driver_championship_standings": iface_driver_championship_standings,
|
74 |
+
"event_info": iface_event_info,
|
75 |
+
"season_calendar": iface_season_calendar,
|
76 |
+
"track_visualization": iface_track_visualization
|
77 |
+
}
|
78 |
|
79 |
+
interface_list = [iface_driver_championship_standings, iface_event_info, iface_season_calendar, iface_track_visualization]
|
80 |
+
tab_names = ["Driver Championship Standings", "Event Info", "Season Calendar", "Track Visualization"]
|
81 |
|
82 |
|
83 |
# Combine all the interfaces into a single TabbedInterface
|
tools.py
CHANGED
@@ -18,7 +18,8 @@ from utils.constants import AVAILABLE_SESSION_TYPES
|
|
18 |
from fastf1.core import Session
|
19 |
from typing import Union
|
20 |
|
21 |
-
from utils import parser_utils
|
|
|
22 |
|
23 |
# Custom types
|
24 |
gp = Union[str, int]
|
@@ -56,10 +57,37 @@ def get_event_info(year: int, round: gp, format: str) -> str:
|
|
56 |
def get_constructor_standings(year: int) -> str:
|
57 |
pass
|
58 |
|
59 |
-
def
|
60 |
pass
|
61 |
|
62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
63 |
|
64 |
if __name__ == "__main__":
|
65 |
session = get_session(2024, 1, "fp1")
|
|
|
18 |
from fastf1.core import Session
|
19 |
from typing import Union
|
20 |
|
21 |
+
from utils import parser_utils, track_utils
|
22 |
+
from PIL import Image
|
23 |
|
24 |
# Custom types
|
25 |
gp = Union[str, int]
|
|
|
57 |
def get_constructor_standings(year: int) -> str:
|
58 |
pass
|
59 |
|
60 |
+
def get_end_of_season_standings(year):
|
61 |
pass
|
62 |
|
63 |
|
64 |
+
def driver_position(driver_name: str) -> str:
|
65 |
+
"""
|
66 |
+
Get the current position of the given driver.
|
67 |
+
|
68 |
+
Args:
|
69 |
+
driver_name (str): The driver's name
|
70 |
+
|
71 |
+
Returns:
|
72 |
+
str: The driver's current position
|
73 |
+
"""
|
74 |
+
return f"Driver {driver_name} is in position {random.randint(1, 20)}"
|
75 |
+
|
76 |
+
def driver_championship_score(year: int, driver_name: str) -> str:
|
77 |
+
pass
|
78 |
+
|
79 |
+
def track_visualization(year: int, round: gp, visualization_type: str, driver_name: str) -> Image:
|
80 |
+
|
81 |
+
session = get_session(year, round, "race")
|
82 |
+
session.load()
|
83 |
+
|
84 |
+
if visualization_type == "speed":
|
85 |
+
return track_utils.create_track_speed_visualization(session, driver_name)
|
86 |
+
elif visualization_type == "corners":
|
87 |
+
return track_utils.create_track_corners_visualization(session)
|
88 |
+
elif visualization_type == "gear":
|
89 |
+
return track_utils.create_track_gear_visualization(session)
|
90 |
+
|
91 |
|
92 |
if __name__ == "__main__":
|
93 |
session = get_session(2024, 1, "fp1")
|
utils/track_utils.py
ADDED
@@ -0,0 +1,212 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import matplotlib as mpl
|
2 |
+
import numpy as np
|
3 |
+
from matplotlib import pyplot as plt
|
4 |
+
from matplotlib.collections import LineCollection
|
5 |
+
import fastf1 as ff1
|
6 |
+
from PIL import Image
|
7 |
+
from io import BytesIO
|
8 |
+
from typing import Union
|
9 |
+
import json
|
10 |
+
|
11 |
+
# Custom types
|
12 |
+
gp = Union[str, int]
|
13 |
+
session_type = Union[str, int, None]
|
14 |
+
|
15 |
+
|
16 |
+
def rotate(xy, *, angle):
|
17 |
+
rot_mat = np.array([[np.cos(angle), np.sin(angle)],
|
18 |
+
[-np.sin(angle), np.cos(angle)]])
|
19 |
+
return np.matmul(xy, rot_mat)
|
20 |
+
|
21 |
+
|
22 |
+
|
23 |
+
def create_track_speed_visualization(session, driver_name: str) -> Image:
|
24 |
+
|
25 |
+
weekend = session.event
|
26 |
+
session.load()
|
27 |
+
with open("assets/driver_abbreviations.json") as f:
|
28 |
+
driver_abbreviations = json.load(f)
|
29 |
+
driver_abbreviation = driver_abbreviations[driver_name]
|
30 |
+
lap = session.laps.pick_drivers(driver_abbreviation).pick_fastest()
|
31 |
+
|
32 |
+
# Get telemetry data
|
33 |
+
x = lap.telemetry['X'] # values for x-axis
|
34 |
+
y = lap.telemetry['Y'] # values for y-axis
|
35 |
+
color = lap.telemetry['Speed'] # value to base color gradient on
|
36 |
+
|
37 |
+
|
38 |
+
points = np.array([x, y]).T.reshape(-1, 1, 2)
|
39 |
+
segments = np.concatenate([points[:-1], points[1:]], axis=1)
|
40 |
+
|
41 |
+
# We create a plot with title and adjust some setting to make it look good.
|
42 |
+
fig, ax = plt.subplots(sharex=True, sharey=True, figsize=(12, 6.75))
|
43 |
+
fig.suptitle(f'{weekend["EventName"]} - {driver_name} ', size=24, y=0.97)
|
44 |
+
|
45 |
+
# Adjust margins and turn of axis
|
46 |
+
plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.12)
|
47 |
+
ax.axis('off')
|
48 |
+
|
49 |
+
# After this, we plot the data itself.
|
50 |
+
# Create background track line
|
51 |
+
ax.plot(lap.telemetry['X'], lap.telemetry['Y'],
|
52 |
+
color='black', linestyle='-', linewidth=16, zorder=0)
|
53 |
+
|
54 |
+
# Create a continuous norm to map from data points to colors
|
55 |
+
norm = plt.Normalize(color.min(), color.max())
|
56 |
+
lc = LineCollection(segments, cmap=mpl.colormaps['viridis'], norm=norm,
|
57 |
+
linestyle='-', linewidth=5)
|
58 |
+
|
59 |
+
# Set the values used for colormapping
|
60 |
+
lc.set_array(color)
|
61 |
+
|
62 |
+
# Merge all line segments together
|
63 |
+
line = ax.add_collection(lc)
|
64 |
+
|
65 |
+
# Finally, we create a color bar as a legend.
|
66 |
+
cbaxes = fig.add_axes([0.25, 0.05, 0.5, 0.05])
|
67 |
+
normlegend = mpl.colors.Normalize(vmin=color.min(), vmax=color.max())
|
68 |
+
legend = mpl.colorbar.ColorbarBase(cbaxes, norm=normlegend, cmap=mpl.colormaps['viridis'],
|
69 |
+
orientation="horizontal")
|
70 |
+
|
71 |
+
# Create a PIL image from the plot
|
72 |
+
fig = plt.gcf()
|
73 |
+
|
74 |
+
# Save the figure to a BytesIO buffer and convert to bytes
|
75 |
+
buf = BytesIO()
|
76 |
+
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
77 |
+
buf.seek(0)
|
78 |
+
|
79 |
+
# Create PIL image from buffer bytes and close the figure
|
80 |
+
img_data = buf.getvalue()
|
81 |
+
plt.close(fig)
|
82 |
+
buf.close()
|
83 |
+
|
84 |
+
# Create new image from the raw bytes
|
85 |
+
img = Image.open(BytesIO(img_data))
|
86 |
+
return img
|
87 |
+
|
88 |
+
|
89 |
+
def create_track_corners_visualization(session) -> Image:
|
90 |
+
|
91 |
+
lap = session.laps.pick_fastest()
|
92 |
+
pos = lap.get_pos_data()
|
93 |
+
|
94 |
+
circuit_info = session.get_circuit_info()
|
95 |
+
|
96 |
+
|
97 |
+
# Get an array of shape [n, 2] where n is the number of points and the second
|
98 |
+
# axis is x and y.
|
99 |
+
track = pos.loc[:, ('X', 'Y')].to_numpy()
|
100 |
+
|
101 |
+
# Convert the rotation angle from degrees to radian.
|
102 |
+
track_angle = circuit_info.rotation / 180 * np.pi
|
103 |
+
|
104 |
+
# Rotate and plot the track map.
|
105 |
+
rotated_track = rotate(track, angle=track_angle)
|
106 |
+
plt.plot(rotated_track[:, 0], rotated_track[:, 1])
|
107 |
+
|
108 |
+
offset_vector = [500, 0] # offset length is chosen arbitrarily to 'look good'
|
109 |
+
|
110 |
+
# Iterate over all corners.
|
111 |
+
for _, corner in circuit_info.corners.iterrows():
|
112 |
+
# Create a string from corner number and letter
|
113 |
+
txt = f"{corner['Number']}{corner['Letter']}"
|
114 |
+
|
115 |
+
# Convert the angle from degrees to radian.
|
116 |
+
offset_angle = corner['Angle'] / 180 * np.pi
|
117 |
+
|
118 |
+
# Rotate the offset vector so that it points sideways from the track.
|
119 |
+
offset_x, offset_y = rotate(offset_vector, angle=offset_angle)
|
120 |
+
|
121 |
+
# Add the offset to the position of the corner
|
122 |
+
text_x = corner['X'] + offset_x
|
123 |
+
text_y = corner['Y'] + offset_y
|
124 |
+
|
125 |
+
# Rotate the text position equivalently to the rest of the track map
|
126 |
+
text_x, text_y = rotate([text_x, text_y], angle=track_angle)
|
127 |
+
|
128 |
+
# Rotate the center of the corner equivalently to the rest of the track map
|
129 |
+
track_x, track_y = rotate([corner['X'], corner['Y']], angle=track_angle)
|
130 |
+
|
131 |
+
# Draw a circle next to the track.
|
132 |
+
plt.scatter(text_x, text_y, color='grey', s=140)
|
133 |
+
|
134 |
+
# Draw a line from the track to this circle.
|
135 |
+
plt.plot([track_x, text_x], [track_y, text_y], color='grey')
|
136 |
+
|
137 |
+
# Finally, print the corner number inside the circle.
|
138 |
+
plt.text(text_x, text_y, txt,
|
139 |
+
va='center_baseline', ha='center', size='small', color='white')
|
140 |
+
|
141 |
+
|
142 |
+
plt.title(session.event['Location'])
|
143 |
+
plt.xticks([])
|
144 |
+
plt.yticks([])
|
145 |
+
plt.axis('equal')
|
146 |
+
|
147 |
+
# Create a PIL image from the plot
|
148 |
+
fig = plt.gcf()
|
149 |
+
|
150 |
+
# Save the figure to a BytesIO buffer and convert to bytes
|
151 |
+
buf = BytesIO()
|
152 |
+
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
153 |
+
buf.seek(0)
|
154 |
+
|
155 |
+
# Create PIL image from buffer bytes and close the figure
|
156 |
+
img_data = buf.getvalue()
|
157 |
+
plt.close(fig)
|
158 |
+
buf.close()
|
159 |
+
|
160 |
+
# Create new image from the raw bytes
|
161 |
+
img = Image.open(BytesIO(img_data))
|
162 |
+
return img
|
163 |
+
|
164 |
+
|
165 |
+
def create_track_gear_visualization(session) -> Image:
|
166 |
+
|
167 |
+
lap = session.laps.pick_fastest()
|
168 |
+
tel = lap.get_telemetry()
|
169 |
+
|
170 |
+
x = np.array(tel['X'].values)
|
171 |
+
y = np.array(tel['Y'].values)
|
172 |
+
|
173 |
+
points = np.array([x, y]).T.reshape(-1, 1, 2)
|
174 |
+
segments = np.concatenate([points[:-1], points[1:]], axis=1)
|
175 |
+
gear = tel['nGear'].to_numpy().astype(float)
|
176 |
+
|
177 |
+
cmap = plt.cm.get_cmap('viridis', 8)
|
178 |
+
norm = plt.Normalize(1, 8)
|
179 |
+
lc_comp = LineCollection(segments, norm=norm, cmap=cmap)
|
180 |
+
lc_comp.set_array(gear)
|
181 |
+
lc_comp.set_linewidth(4)
|
182 |
+
|
183 |
+
plt.gca().add_collection(lc_comp)
|
184 |
+
plt.axis('equal')
|
185 |
+
plt.tick_params(labelleft=False, left=False, labelbottom=False, bottom=False)
|
186 |
+
|
187 |
+
plt.suptitle(
|
188 |
+
f"Fastest Lap Gear Shift Visualization\n"
|
189 |
+
f"{lap['Driver']} - {session.event['EventName']}"
|
190 |
+
)
|
191 |
+
|
192 |
+
cbar = plt.colorbar(mappable=lc_comp, label="Gear")
|
193 |
+
cbar.set_ticks(np.arange(1, 9)) # Set ticks at integer positions
|
194 |
+
cbar.set_ticklabels(np.arange(1, 9)) # Labels from 1 to 8
|
195 |
+
|
196 |
+
# Create a PIL image from the plot
|
197 |
+
fig = plt.gcf()
|
198 |
+
|
199 |
+
# Save the figure to a BytesIO buffer and convert to bytes
|
200 |
+
buf = BytesIO()
|
201 |
+
fig.savefig(buf, format='png', dpi=150, bbox_inches='tight')
|
202 |
+
buf.seek(0)
|
203 |
+
|
204 |
+
# Create PIL image from buffer bytes and close the figure
|
205 |
+
img_data = buf.getvalue()
|
206 |
+
plt.close(fig)
|
207 |
+
buf.close()
|
208 |
+
|
209 |
+
# Create new image from the raw bytes
|
210 |
+
img = Image.open(BytesIO(img_data))
|
211 |
+
return img
|
212 |
+
|