Skip to content

File Formats

Michał Pełka edited this page Sep 25, 2026 · 1 revision

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.

Overview

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

Timestamps and units

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.

Raw session (Mandeye recording)

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).

lidarNNNN.laz

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 with gps_time == 0 are skipped.
  • intensity: reflectivity, 0–255.
  • user_data: LiDAR id, the key into lidarNNNN.sn.
  • classification: driver tag; ignored.

imuNNNN.csv

Two layouts are accepted:

  • With a header: delimited by spaces, commas or tabs. Required columns are timestamp, gyroX, gyroY, gyroZ, accX, accY, accZ and timestampUnix; imuId is 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).

lidarNNNN.sn

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.

calibration.json / *.mjc

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.

gnssNNNN.gnss

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.

*.nmea

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.

CAMERA_0/

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.

Step 1 output (lio_result_N/)

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.

session.mjs

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 (and offset_to_apply_x/y/z after 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 adds exporting_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 only file_name)
  • loop_closure_edges[] (step 2): index_from, index_to (scan indices), relative pose px py pz (m) and om fi ka (rad, Tait-Bryan), weights w_px … w_ka, flags is_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

scan_lio_N.laz

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_time is in nanoseconds (the raw LAZ uses seconds).
  • intensity is kept; return_number is 1; the LiDAR id is not stored.

trajectory_lio_N.csv

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.

session_poses.mrp / session_ini_poses.mri

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
...
  • .mri holds the initial poses (the "before" state, used by Revert to initial); .mrp holds 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 prints PROBLEM 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 .mrp before running step 3: Save results overwrites the .mrp of every session that is not marked as ground truth.

Other files

  • 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_info with CPU/RAM/OS and timings, trajectory_results, motion_model_correction, NDT grid settings, transformation_matrix).

Step 3 project (*.mjp)

JSON, saved by Save project:

  • session_file_names[]: {"session_file_name": "<absolute path to session.mjs>"}
  • loop_closure_edges[]: same fields as the edges in session.mjs, plus index_session_from / index_session_to (indices into session_file_names). index_from / index_to are 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.

Camera calibration files

See Tools for coloring and georeferencing for the workflow.

Intrinsics (.yaml)

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 keys width, height, fx, fy, cx, cy, distortion_model, distortion (and xi for Mei). The order of distortion depends on the model:

    distortion_model Camera model distortion
    insta360_mei_v2 Mei k1 k2 k3 p1 p2 (+ xi key), not OpenCV order
    equidistant / fisheye Fisheye (OpenCV cv::fisheye) k1 k2 k3 k4, exactly 4
    plumb_bob Pinhole k1 k2 p1 p2 k3
    rational_polynomial Pinhole k1 k2 p1 p2 k3 k4 k5 k6

    The k, r and p matrices in a camera_info.yaml are plain pinhole matrices built from fx/fy/cx/cy (for ROS CameraInfo). They do not contain xi or distortion. rotation_deg / translation are the lens pose reported by the camera firmware, not a camera-LiDAR extrinsic.

LiDAR-camera calibration (.json)

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.

Colored point cloud export

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.

Clone this wiki locally