-
Notifications
You must be signed in to change notification settings - Fork 70
File Formats
Reference for the files HDMapping reads and writes: the raw Mandeye recording, the output of step 1
(lidar_odometry_step_1), step 2 (multi_view_tls_registration_step_2) and step 3
(multi_session_registration_step_3), and the camera calibration / coloring tools.
| File | Written by | Read by |
|---|---|---|
lidarNNNN.laz, imuNNNN.csv, lidarNNNN.sn
|
Mandeye controller, mcap_to_laz
|
raw data viewer, step 1 |
gnssNNNN.gnss, *.nmea
|
Mandeye controller | step 2 (GNSS menu) |
calibration.json / *.mjc
|
mandeye_mission_recorder_calibration, by hand |
step 1, laz_to_mcap
|
session.mjs |
step 1, step 2 | step 2, step 3, mandeye_single_session_viewer, camera_lidar_trajectory_viewer, session_to_mcap
|
scan_lio_N.laz, trajectory_lio_N.csv
|
step 1 | every tool that opens a session.mjs
|
session_poses.mrp, session_ini_poses.mri
|
step 1, step 2, step 3 (.mrp only) |
every tool that opens a session.mjs
|
project.mjp |
step 3 | step 3 |
intrinsics.yaml, camera_info.yaml
|
camera_lidar_intrinsics_calib, Insta360 converter |
camera_lidar_calibration |
Calibration JSON (extrinsics.json, cam_<lens>.json) |
camera_lidar_calibration |
camera_lidar_calibration, camera_lidar_trajectory_viewer
|
A typical folder after step 1:
my_session/
├── lidar0000.laz imu0000.csv lidar0000.sn
├── lidar0001.laz imu0001.csv lidar0001.sn
├── ...
├── gnss0000.gnss (or *.nmea on newer controllers)
├── calibration.json (multi-LiDAR setups only)
├── CAMERA_0/ (builds with a camera)
├── cache/ preview/ (created by step 1)
└── lio_result_0/
├── session.mjs
├── session_poses.mrp session_ini_poses.mri
├── scan_lio_0.laz trajectory_lio_0.csv
├── ...
├── point_sizes_per_chunk.json
├── HDMapping_params_<ver>_<date>.toml
└── HDMapping_results_<ver>_<date>.json
All sensors share the LiDAR (device) clock. It can be time from boot for system that has no synchronization (one LiDAR), or Unix timestamp for multi-LiDAR devices or LiDAR-camera units.
| File | Field | Unit |
|---|---|---|
lidarNNNN.laz (raw) |
gps_time |
seconds, LiDAR clock |
imuNNNN.csv |
timestamp / timestampUnix
|
nanoseconds, LiDAR clock + Unix |
gnssNNNN.gnss |
1st column | nanoseconds, LiDAR clock |
*.nmea |
1st / 2nd column | nanoseconds, LiDAR clock / milliseconds, Unix |
scan_lio_N.laz (step 1) |
gps_time |
nanoseconds, LiDAR clock |
trajectory_lio_N.csv |
timestamp_nanoseconds / timestampUnix_nanoseconds
|
nanoseconds, LiDAR clock |
CAMERA_0/*.jpg |
file name or .meta.json sidecar |
nanoseconds , Unix |
Gyroscope readings are in rad/s and accelerometer readings in g, so |acc| ≈ 1 at rest.
The recording is split into chunks, 20 s each by default. LiDAR and IMU files are paired by the last
4 digits of the file name (lidar0007.laz ↔ imu0007.csv).
LAS 1.2, point format 1, scale 1e-4, offset 0.
-
x,y,z: in the LiDAR frame, not undistorted. -
gps_time: seconds on the LiDAR clock. Points withgps_time == 0are skipped. -
intensity: reflectivity, 0–255. -
user_data: LiDAR id, the key intolidarNNNN.sn. -
classification: driver tag; ignored.
Two layouts are accepted:
-
With a header: delimited by spaces, commas or tabs. Required columns are
timestamp,gyroX,gyroY,gyroZ,accX,accY,accZandtimestampUnix;imuIdis optional.timestamp gyroX gyroY gyroZ accX accY accZ imuId timestampUnix -
Headerless (legacy): space-separated
timestamp gx gy gz ax ay az [imuId [timestampUnix]]. Recordings often have 8 columns (no Unix time).
When the file has several IMUs, only rows whose imuId matches imuToUse from the calibration file are
used (default 0).
Maps the LiDAR id (user_data in the LAZ) to the sensor serial number, one pair per line:
0 47MDL4F0010072
Only the first .sn file found is read.
Needed only for multi-LiDAR setups. .mjc is the same JSON, written by
mandeye_mission_recorder_calibration. It holds one 4×4 extrinsic per LiDAR serial and names the
IMU to use:
{
"calibration": {
"47MDL4F0010072": { "identity": "true" },
"47MDL4F0010089": {
"order": "ROW",
"inverted": "FALSE",
"data": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0.12, 0, 0, 0, 1]
}
},
"imuToUse": "47MDL4F0010072"
}order is ROW or COLUMN (layout of data). inverted says whether the matrix has to be inverted
before it is applied. Any other *.json in the folder, except status*, cam0* and cam1*, is
taken as the calibration file. Without calibration and .sn files, a single LiDAR with IMU id 0 is
assumed. The "There is no calibration file" message is expected in that case.
One GGA fix per line, space-separated, no header:
timestamp lat lon alt hdop satellites undulation age time fix_quality timestampUnix
54651848940 52.1073 21.0268 122.2 1.25 8 35.8 nan 14:51:42 1 1747581809550
-
timestamp: nanoseconds on the LiDAR clock;timestampUnix: milliseconds, Unix (not read by HDMapping; only the first 10 fields are required). -
lat,lon: WGS-84 decimal degrees;alt: altitude above mean sea level (m);undulation: geoid separation (m). -
time: UTC time of the fix;fix_quality: GGA fix quality (1 = GPS, 2 = DGPS, 4 = RTK fixed, 5 = RTK float).
Rows with lat/lon equal to 0 or non-finite are dropped. Empty (0-byte) files are normal
indoors or when there was no fix.
Newer controllers write raw NMEA sentences prefixed with two timestamps:
<timestampLidar_ns> <timestampUnix_ms> <NMEA sentence>
11984423415770 1747581809550 $GNGGA,092656.000,5036.007407,N,...*76
Checksums are validated. Only $GNGGA sentences produce positions. Height = altitude + geoid
undulation.
Images from the synchronized camera: lowercase .jpg, named <prefix>_<timestamp_ns>.jpg or
<timestamp_ns>.jpg. An optional sidecar <name>.meta.json (libcamera metadata) overrides the
timestamp with its FRAME_WALL_CLOCK key. Timestamps must be on the LiDAR clock. Frames named by
index (00011.jpg) are parsed as a timestamp of 11 ns and get colored with the wrong poses.
Paths inside session.mjs are absolute. When a stored path no longer exists, the loader looks for
a file with the same name next to the .mjs, so a lio_result_N folder can be moved as a whole.
JSON description of a processed session.
-
"Session Settings"-
poses_file_name(.mrp),initial_poses_file_name(.mri),out_poses_file_name,folder_name,out_folder_name -
offset_x/y/z(andoffset_to_apply_x/y/zafter step 2): offset subtracted from large georeferenced coordinates -
ground_truth(bool): marks the reference session in step 3. Step 1 does not write it; set it in step 2 or by hand. - step 1 metadata:
lidar_odometry_version,length of trajectory[m],elapsed time seconds,decimation,threshold_nr_poses,point_sizes_path; step 2 addsexporting_software_version
-
-
laz_file_names[]:{file_name, fixed_x/y/z, fixed_om/fi/ka, fuse_inclination_from_IMU}, one per scan (step 1 writes onlyfile_name) -
loop_closure_edges[](step 2):index_from,index_to(scan indices), relative posepx py pz(m) andom fi ka(rad, Tait-Bryan), weightsw_px … w_ka, flagsis_fixed_px … is_fixed_ka -
ground_control_points[]:name, x, y, z, sigma_x/y/z, lidar_height_above_ground, index_to_node_inner/outer -
control_points[]:name, x/y/z_source_local, x/y/z_target_global, sigma_x/y/z, index_to_pose, is_z_0
One file per ~5 s of data. LAS 1.2, point format 1, scale 1e-4.
- Points are undistorted and in the scan's local frame: world point = pose of the scan from the
.mrp× point. -
gps_timeis in nanoseconds (the raw LAZ uses seconds). -
intensityis kept;return_numberis 1; the LiDAR id is not stored.
Space-separated with a header, one row per LiDAR odometry pose:
timestamp_nanoseconds pose00 pose01 pose02 pose03 pose10 pose11 pose12 pose13 pose20 pose21 pose22 pose23 timestampUnix_nanoseconds om_rad fi_rad ka_rad
-
pose00 … pose23: 3×4 row-major pose relative to the frame of scan N (the first row is close to identity). -
om_rad fi_rad ka_rad: roll / pitch / yaw from the IMU AHRS, in radians. - Timestamps are written as floating point and can have a fractional part
(
548348730189.99993896). Parse them as floats and round; parsing as an integer stops at the.and shifts every following column.
Plain text with the same layout: the number of scans, then for each scan its file name and 4×4 world pose.
149
scan_lio_0.laz
r11 r12 r13 tx
r21 r22 r23 ty
r31 r32 r33 tz
0 0 0 1
scan_lio_1.laz
...
-
.mriholds the initial poses (the "before" state, used by Revert to initial);.mrpholds the current poses, which all tools display and export. Both are identical after step 1. Pose 0 is gravity-aligned from the IMU, not identity. - Poses are matched to scans by file name. If the pose count differs from the number of scans in the
.mjs, the log printsPROBLEM point_clouds.size() != pcs.size()and the poses are not applied. - Step 2 and step 3 write refined poses to the
.mrp; step 3 never writes the.mri. Back up the.mrpbefore running step 3: Save results overwrites the.mrpof every session that is not marked as ground truth.
-
point_sizes_per_chunk.json: JSON array with the point count of each odometry node. -
HDMapping_params_<ver>_<date>.toml: the parameters used for the run. Pass it back to step 1 to reproduce the run. -
HDMapping_results_<ver>_<date>.json: run report (directory_info,processing_infowith CPU/RAM/OS and timings,trajectory_results,motion_model_correction, NDT grid settings,transformation_matrix).
JSON, saved by Save project:
-
session_file_names[]:{"session_file_name": "<absolute path to session.mjs>"} -
loop_closure_edges[]: same fields as the edges insession.mjs, plusindex_session_from/index_session_to(indices intosession_file_names).index_from/index_toare scan indices within those sessions.
The project stores nothing else. Poses stay in each session's .mrp, and the ground-truth flag is
read from each session's session.mjs.
See Tools for coloring and georeferencing for the workflow.
camera_lidar_calibration accepts two YAML layouts:
-
OpenCV FileStorage, written by
camera_lidar_intrinsics_calib:image_width,image_height,camera_matrix,distortion_model: rational_polynomial,distortion_coefficients,avg_reprojection_error. -
Flat
camera_info.yaml(e.g. Insta360 lenses): top-level keyswidth,height,fx,fy,cx,cy,distortion_model,distortion(andxifor Mei). The order ofdistortiondepends on the model:distortion_modelCamera model distortioninsta360_mei_v2Mei k1 k2 k3 p1 p2(+xikey), not OpenCV orderequidistant/fisheyeFisheye (OpenCV cv::fisheye)k1 k2 k3 k4, exactly 4plumb_bobPinhole k1 k2 p1 p2 k3rational_polynomialPinhole k1 k2 p1 p2 k3 k4 k5 k6The
k,randpmatrices in acamera_info.yamlare plain pinhole matrices built from fx/fy/cx/cy (for ROSCameraInfo). They do not containxior distortion.rotation_deg/translationare the lens pose reported by the camera firmware, not a camera-LiDAR extrinsic.
Written by camera_lidar_calibration, read by it and by camera_lidar_trajectory_viewer --calib.
"World" here is the LiDAR frame.
| Key | Meaning |
|---|---|
camera.{frame_id, model, serial} |
camera identity |
lidar.serial |
LiDAR serial, "unknown" if the point cloud carried none |
intrinsics.model |
pinhole, mei or fisheye (also read: equidistant, insta360_mei_v2). Any other value loads as pinhole |
intrinsics.{fx, fy, cx, cy, xi, k1 … k6, p1, p2} |
intrinsics, one key per parameter. Fisheye uses k1 … k4; pinhole uses the rational model k1 … k6, p1, p2
|
intrinsics.{width, height} |
resolution the intrinsics apply to (0 = unknown); images of another size are scaled |
extrinsics.camera_position_in_world_xyz |
camera centre C in the LiDAR frame (m) |
extrinsics.camera_rotation_matrix_in_world |
R, 3×3 row-major: camera axes in LiDAR coordinates. Read on load |
extrinsics.T_lidar_to_camera_4x4 |
[Rᵀ | −Rᵀ·C], so p_cam = T · p_lidar. Written for convenience and ignored on load, so editing only this field has no effect |
Equivalently, p_lidar = R · p_cam + C.
camera_lidar_trajectory_viewer exports LAZ in point format 3 with 16-bit RGB (8-bit color
shifted left by 8), intensity scaled to 0–65535 and gps_time in seconds. Points that were not
colored get an intensity grey. Enable LAZ: only points with valid color to leave them out.
The LAZ export in step 2 uses point format 1 and does not carry RGB.