GPS CSV to KITTI
This script converts GPS measurments from frames/gps.csv into KITTI-format poses.txt, producing one 3x4 pose matrix per frame.GPS poses are interpolated to each frame timestamp using frames_manifest.csv.
Overview
The script performs the following steps:
-
Load GPSsamples from
frames/gps.csv -
Load frame timestamps from
frames/frames_manifest.csv - Interpolate GPS position and heading from each frame
- Convert latitude, longitude, and altitude into local coordinates
- Build KITTI 3x4 pose matrix for each frame
- Write the result to
frames/poses.txt
Coordinate System
The Output using following coordinate mapping: - x = East - y = Down - z = North
The translation ver´ctor is defined as:
t= [east, -up, north]
This matches a trajectory view where x and z from the horizontal plane.
Input Files
frames/gps.csv
Expected columns:
- timestamp_ns
- latitude
- longitude
- altitude_m
frames/frames_manifest.csv
Expected columns:
cam0_ts_nscam1_ts_ns
The frame timestamp is computed as the average of the two camera timestamps:
ts = (int(row["cam0_ts_ns"])+ int(row["cam1_ts_ns"])) / 2
Output File
frames/poses.txt
Each line contains one flattened 3x4 KITTI pose matrix:
r11 r12 r13 tx r21 r22 r23 ty r31 r32 r33 tz
Code
"""
Convert frames/gps.csv to KITTI-format poses.txt (one 3x4 matrix per frame).
Coordinate mapping (matches plot_trajectory X vs Z = horizontal plane):
X = East, Y = -(altitude above origin, i.e. down), Z = North
Rotation from GPS heading (clockwise from North) → yaw around Y-down axis.
GPS poses are interpolated to each frame timestamp from frames_manifest.csv.
"""
import csv
import math
import numpy as np
EARTH_R = 6_371_000.0 # metres
def lat_lon_alt_to_enu(lat, lon, alt, lat0, lon0, alt0):
dlat = math.radians(lat - lat0)
dlon = math.radians(lon - lon0)
north = dlat * EARTH_R
east = dlon * EARTH_R * math.cos(math.radians(lat0))
up = alt - alt0
return east, north, up
def heading_to_rotation(heading_deg):
"""Yaw-only rotation around Y-down axis from compass heading (CW from North)."""
theta = math.radians(heading_deg)
c, s = math.cos(theta), math.sin(theta)
return np.array([
[ c, 0, s],
[ 0, 1, 0],
[-s, 0, c],
], dtype=np.float64)
def interp_angle(a0, a1, t):
"""Linear interpolation of angles with wraparound (degrees)."""
diff = ((a1 - a0 + 180) % 360) - 180
return (a0 + diff * t) % 360
gps_rows = []
with open("frames/gps.csv") as f:
for row in csv.DictReader(f):
gps_rows.append({
"ts": int(row["timestamp_ns"]),
"lat": float(row["latitude"]),
"lon": float(row["longitude"]),
"alt": float(row["altitude_m"]),
"heading": float(row["heading_deg"]),
})
gps_rows.sort(key=lambda r: r["ts"])
lat0 = gps_rows[0]["lat"]
lon0 = gps_rows[0]["lon"]
alt0 = gps_rows[0]["alt"]
gps_ts = np.array([r["ts"] for r in gps_rows], dtype=np.float64)
frame_ts = []
with open("frames/frames_manifest.csv") as f:
for row in csv.DictReader(f):
ts = (int(row["cam0_ts_ns"]) + int(row["cam1_ts_ns"])) / 2
frame_ts.append(ts)
def interp_gps(query_ts):
idx = np.searchsorted(gps_ts, query_ts)
if idx == 0:
return gps_rows[0]
if idx >= len(gps_rows):
return gps_rows[-1]
r0, r1 = gps_rows[idx - 1], gps_rows[idx]
t = (query_ts - r0["ts"]) / (r1["ts"] - r0["ts"])
return {
"lat": r0["lat"] + t * (r1["lat"] - r0["lat"]),
"lon": r0["lon"] + t * (r1["lon"] - r0["lon"]),
"alt": r0["alt"] + t * (r1["alt"] - r0["alt"]),
"heading": interp_angle(r0["heading"], r1["heading"], t),
}
poses = []
for ts in frame_ts:
g = interp_gps(ts)
east, north, up = lat_lon_alt_to_enu(g["lat"], g["lon"], g["alt"], lat0, lon0, alt0)
t = np.array([east, -up, north], dtype=np.float64)
R = heading_to_rotation(g["heading"])
T = np.eye(3, 4, dtype=np.float64)
T[:3, :3] = R
T[:3, 3] = t
poses.append(T)
out_path = "frames/poses.txt"
with open(out_path, "w") as f:
for T in poses:
line = " ".join(f"{v:.6e}" for v in T.flatten())
f.write(line + "\n")
print(f"Written {len(poses)} poses → {out_path}")
print(f"Origin: lat={lat0:.6f}, lon={lon0:.6f}, alt={alt0:.1f} m")
east_range = max(T[0,3] for T in poses) - min(T[0,3] for T in poses)
north_range = max(T[2,3] for T in poses) - min(T[2,3] for T in poses)
print(f"East span: {east_range:.1f} m, North span: {north_range:.1f} m")
Function Details
lat_lon_alt_to_enu
Converts latitude, longitude, and altitude into local coordinates relative to the first GPS sample.
def lat_lon_alt_to_enu(lat, lon, alt, lat0, lon0, alt0):
dlat = math.radians(lat - lat0)
dlon = math.radians(lon - lon0)
north = dlat * EARTH_R
east = dlon * EARTH_R * math.cos(math.radians(lat0))
up = alt - alt0
return east, north, up
heading_to_rotation
Converts GPS heading into a yaw-only rotation matrix around the Y-down axis.
def heading_to_rotation(heading_deg):
theta = math.radians(heading_deg)
c, s = math.cos(theta), math.sin(theta)
return np.array([
[ c, 0, s],
[ 0, 1, 0],
[-s, 0, c],
], dtype=np.float64)
interp_angle
Interpolates heading values while handling wraparound at 360 degrees.
def interp_angle(a0, a1, t):
diff = ((a1 - a0 + 180) % 360) - 180
return (a0 + diff * t) % 360
interp_gps
Interpolates GPS position and heading for a given frame timestamp.
def interp_gps(query_ts):
idx = np.searchsorted(gps_ts, query_ts)
if idx == 0:
return gps_rows[0]
if idx >= len(gps_rows):
return gps_rows[-1]
r0, r1 = gps_rows[idx - 1], gps_rows[idx]
t = (query_ts - r0["ts"]) / (r1["ts"] - r0["ts"])
return {
"lat": r0["lat"] + t * (r1["lat"] - r0["lat"]),
"lon": r0["lon"] + t * (r1["lon"] - r0["lon"]),
"alt": r0["alt"] + t * (r1["alt"] - r0["alt"]),
"heading": interp_angle(r0["heading"], r1["heading"], t),
}
Usage
Run the script with:
python gps_to_kitti.py
Example Console Output
Written 250 poses → frames/poses.txt
Origin: lat=50.123456, lon=6.123456, alt=145.2 m
East span: 85.3 m, North span: 122.7 m
Notes
- The Earth is approximated as a sphere
- This method is suitable for small local areas
- Only yaw is used for orientation
- Roll and pitch are ignored
- Frame timestamps outside the GPS range use the nearest GPS sample