Computer-Vision

F1 new track limit detection system — How they do it

F1 new track limit detection system — How they do it

Introduced for the 2026 season, the FIA's new ECAT (Every Car, All Turns) system continuously monitors track limits using AI and computer‑vision technology. Video‑based car recognition. System recognizes each car's silhouette and compares it to camera reference points. It determines if the car's wheels cross the white line that defines the track boundary.

To illustrate the geofencing logic — the same conceptual building block used by the FIA's new system — the example below: Builds two virtual geofences at the exit of right‑handers. Generates a synthetic lap with a car that runs slightly wide. Computes wheel positions from car CG and yaw.
Flags a violation as soon as any wheel enters a geofence (we could enforce the stricter "all four beyond" criterion by modeling the white line as a polygon and checking wheel centers vs. line). The formal definition is the all‑four‑wheels rule, used by stewards; we keep the demo simple. Debounces contiguous frames into a single event, counts strikes, and illustrates a basic penalty threshold. Note: Real ECAT uses multi‑camera vision and car recognition, not a 2D toy model; this is just the geometric/geofence part that's conceptually similar to the virtual zones described by the FIA.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.path import Path
# -----------------------------
# Parameters & simple geometry
# -----------------------------
# Approx F1 car geometry (meters)
CAR_LENGTH = 5.5
CAR_WIDTH = 2.0
WHEELBASE = 3.6
TRACK = 1.6 # distance between left/right wheels
# Wheel offsets in car body frame (origin at CG; x forward, y left)
front_axle_x = WHEELBASE / 2
rear_axle_x = -WHEELBASE / 2
wheel_y = TRACK / 2
WHEELS_BODY = np.array([
 [ front_axle_x,  wheel_y],  # FL
 [ front_axle_x, -wheel_y],  # FR
 [ rear_axle_x,   wheel_y],  # RL
 [ rear_axle_x,  -wheel_y],  # RR
])
# -----------------------------
# Geofenced off-track zones
# -----------------------------
def rect_poly(x0, y0, w, h):
    return np.array([[x0, y0], [x0+w, y0], [x0+w, y0+h], [x0, y0+h]])
# Track drawing aids (not used for logic)
track_polys = []
track_polys.append(rect_poly(-20, -4, 40, 8))  # main straight
track_polys.append(rect_poly(20, -4, 12, 8))   # corner entry
track_polys.append(rect_poly(20, -4, 8, 12))   # corner body
track_polys.append(rect_poly(28, 4, 22, 8))    # short straight
track_polys.append(rect_poly(50, 4, 12, 12))   # fast right area
# Two geofences at exits (tune sizes/placement)
GEOFENCE_1 = rect_poly(26, 8.5, 8, 3.5)
GEOFENCE_2 = rect_poly(56, 15, 8, 3)
def in_polygon(poly, points):
    return Path(poly).contains_points(points)
# -----------------------------
# Synthetic lap (x, y, heading)
# -----------------------------
pos, heading = [], []
# Segment 1: main straight
for t in np.linspace(0, 1, 60):
    x = -20 + 40*t
    y = 0 + 0.2*np.sin(2*np.pi*t)
    yaw = 0.0
    pos.append([x, y]); heading.append(yaw)
# Segment 2: right-hander (center ~ (28,4))
center = np.array([28.0, 4.0]); radius = 8.0
for a in np.linspace(-np.pi/2, 0.2, 80):
    x = center[0] + (radius + 0.6)*np.cos(a)  # run slightly wide at exit
    y = center[1] + (radius + 0.2)*np.sin(a)
    yaw = a + np.pi/2
    pos.append([x, y]); heading.append(yaw)
# Segment 3: short straight
for t in np.linspace(0, 1, 50):
    x = 28 + 22*t
    y = 8 + 0.4*np.sin(3*np.pi*t)
    yaw = 0.0
    pos.append([x, y]); heading.append(yaw)
# Segment 4: fast right kink (center ~ (56,12))
center2 = np.array([56.0, 12.0]); radius2 = 7.0
for a in np.linspace(-np.pi/2, -0.1, 70):
    x = center2[0] + (radius2 + 0.7)*np.cos(a)  # run wide at exit
    y = center2[1] + (radius2 + 0.6)*np.sin(a)
    yaw = a + np.pi/2
    pos.append([x, y]); heading.append(yaw)
pos = np.array(pos); heading = np.array(heading)
# -----------------------------
# Wheel positions per frame
# -----------------------------
def wheels_world(xy, yaw):
    c, s = np.cos(yaw), np.sin(yaw)
    R = np.array([[c, -s], [s, c]])
    return xy + (R @ WHEELS_BODY.T).T
# -----------------------------
# Geofence evaluation
# -----------------------------
g1_inside, g2_inside, any_inside = [], [], []
for k in range(len(pos)):
    wh = wheels_world(pos[k], heading[k])
    g1 = in_polygon(GEOFENCE_1, wh).any()
    g2 = in_polygon(GEOFENCE_2, wh).any()
    g1_inside.append(g1); g2_inside.append(g2)
    any_inside.append(g1 or g2)
g1_inside  = np.array(g1_inside)
g2_inside  = np.array(g2_inside)
any_inside = np.array(any_inside)
# Debounce contiguous frames to events
violations, active, start_idx = [], False, None
for i, inside in enumerate(any_inside):
    if inside and not active:
        active = True; start_idx = i
    elif not inside and active:
        active = False; violations.append((start_idx, i-1))
if active: violations.append((start_idx, len(any_inside)-1))
# Strike/penalty model
STRIKE_LIMIT = 3
num_strikes, penalties = 0, []
for (a, b) in violations:
    num_strikes += 1
    if num_strikes > STRIKE_LIMIT:
        penalties.append((a, b))
# ---- Plot
fig, ax = plt.subplots(figsize=(10, 7))
for poly in track_polys:
    ax.fill(poly[:,0], poly[:,1], color="#D0D8E2", alpha=0.8, lw=0)
ax.fill(GEOFENCE_1[:,0], GEOFENCE_1[:,1], color="#FF6B6B", alpha=0.35, label="Geofence 1")
ax.fill(GEOFENCE_2[:,0], GEOFENCE_2[:,1], color="#FFA94D", alpha=0.35, label="Geofence 2")
ax.plot(pos[:,0], pos[:,1], 'k-', lw=2, label="Car path (CG)")
for (a, b) in violations:
    ax.plot(pos[a:b+1,0], pos[a:b+1,1], color="#D00000", lw=4,
            label="Flagged segment" if "Flagged segment" not in [l.get_label() for l in ax.lines] else "")
# Draw a few wheel footprints
for k in np.linspace(0, len(pos)-1, 8, dtype=int):
    ax.plot(*wheels_world(pos[k], heading[k]).T, 'o', color="#343A40", ms=3)
ax.set_aspect('equal', adjustable='box')
ax.set_title("F1 Track-Limit Geofencing Simulation (virtual zones at two exits)")
ax.set_xlabel("x (m)"); ax.set_ylabel("y (m)")
ax.legend(loc='upper left'); ax.grid(True, ls=':', alpha=0.5)
plt.tight_layout()
plt.show()
print("Detected violations (start_idx, end_idx):", violations)
print("Total strikes:", len(violations))
print("Penalties applied to events:" if penalties else "No time penalties (<= strike limit).", penalties if penalties else "")
Detected violations (start_idx, end_idx): [(140, 157)]
Total strikes: 1
No time penalties (<= strike limit).

Bottom Line

F1's new system couples computer vision with geofenced virtual zones to spot track‑limit breaches immediately, then funnels only the edge cases to human stewards — restoring consistency and speed to an area that had become chaotic. With a few dozen lines of Python, you can prototype the same geofence logic used at the heart of the real‑world system — and then iterate toward the full fidelity of the FIA's ECAT approach.