arre99 commited on
Commit
6bdfadb
·
1 Parent(s): 4ba30cf

track visualization for speed, corner and gears

Browse files
Files changed (3) hide show
  1. app.py +35 -41
  2. tools.py +30 -2
  3. 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
- iface2 = gr.Interface(
55
- fn=driver_position,
56
- inputs=gr.Dropdown(driver_names),
 
 
 
57
  outputs="text",
58
- title="[WIP] Driver Position",
59
- description="Get the current position of the given driver"
60
  )
61
 
62
- iface3 = gr.Interface(
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
- iface4 = gr.Interface(
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 = [iface1, iface2, iface3, iface4]
86
- tab_names = ["[WIP] Driver Championship Score", "[WIP] Driver Position", "Event Info", "Season Calendar"]
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 get_driver_standings(year):
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
+