Metadata-Version: 2.4
Name: kitti2mcap
Version: 0.1.0
Summary: Convert KITTI raw drives to self-contained ROS2 MCAP files (no ROS install required)
Author-email: Aditya Srinivas Manohar <adityasrinivasmanohar@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: autonomous-driving,foxglove,kitti,lidar,mcap,point-cloud,robotics,ros2
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Requires-Python: >=3.11
Requires-Dist: mcap-ros2-support>=0.5.7
Requires-Dist: mcap>=1.3.1
Requires-Dist: numpy>=2.4.6
Requires-Dist: opencv-python-headless>=4.13.0.92
Requires-Dist: pillow>=12.2.0
Requires-Dist: pykitti>=0.3.1
Description-Content-Type: text/markdown

# kitti2mcap

Convert a [KITTI raw](https://www.cvlibs.net/datasets/kitti/raw_data.php) drive
into a single, self-contained ROS2 **MCAP** file — **no ROS installation
required**.

The output carries all the standard sensor topics (camera, LiDAR, IMU, TF) and
embeds every ROS2 message schema inline, so it opens directly in any MCAP-based
tool — [Foxglove](https://foxglove.dev/), the [mcap
CLI](https://github.com/foxglove/mcap), and most calibration/visualization
pipelines — without a sourced ROS workspace.

---

## Install

Uses [uv](https://docs.astral.sh/uv/):

```bash
uv sync
```

This creates a virtual environment and installs the pinned dependencies from
[`pyproject.toml`](pyproject.toml): `pykitti`, `mcap`, `mcap-ros2-support`,
`numpy`, `Pillow`, and `opencv-python-headless` (pulled in by pykitti's package
import).

---

## Usage

```bash
uv run kitti2mcap --date 2011_09_26 --drive 0001 --kitti-dir ./data --output out.mcap
```

| Flag | Description |
|---|---|
| `--date` | KITTI date folder (e.g. `2011_09_26`) |
| `--drive` | Drive sequence number (e.g. `0001`) |
| `--kitti-dir` | Root directory containing the date folder |
| `--output` | Output `.mcap` path |
| `--frames` | Number of frames to convert (default: all) |
| `--cameras` | Cameras to include, e.g. `0,1` (default: `2` — left color) |
| `--no-lidar` | Skip velodyne data |
| `--no-imu` | Skip IMU/OXTS data |

**Examples**

```bash
# First 5 frames only, quick smoke test
uv run kitti2mcap --date 2011_09_26 --drive 0001 --kitti-dir ./data --output test.mcap --frames 5

# Both grayscale + color left cameras, no IMU
uv run kitti2mcap --date 2011_09_26 --drive 0001 --kitti-dir ./data --output out.mcap --cameras 0,2 --no-imu
```

You can also run it as a module: `uv run python -m kitti2mcap ...`.

### Camera indices

| Index | Frame | Topic basename | Encoding |
|---|---|---|---|
| `0` | `camera_gray_left` | `camera_gray_left` | `mono8` |
| `1` | `camera_gray_right` | `camera_gray_right` | `mono8` |
| `2` | `camera_color_left` | `camera_color_left` | `rgb8` |
| `3` | `camera_color_right` | `camera_color_right` | `rgb8` |

---

## Expected input layout

`--kitti-dir` must follow pykitti's canonical layout — calibration files live
*inside* the date folder, alongside the drive:

```
<kitti-dir>/
└── 2011_09_26/
    ├── calib_cam_to_cam.txt
    ├── calib_imu_to_velo.txt
    ├── calib_velo_to_cam.txt
    └── 2011_09_26_drive_0001_sync/
        ├── image_00/  image_01/  image_02/  image_03/
        ├── oxts/
        └── velodyne_points/
```

Downloading a drive + its calibration from the KITTI raw archive and unzipping
both into the same directory produces exactly this structure.

---

## Topics written

| Topic | Type | Notes |
|---|---|---|
| `/kitti/camera_color_left/image_raw` | `sensor_msgs/Image` | Camera 2 (left color), `rgb8`. Other cameras via `--cameras` |
| `/kitti/camera_color_left/camera_info` | `sensor_msgs/CameraInfo` | Rectified intrinsics (`P_rect` / `R_rect` / `K`) |
| `/kitti/velodyne_points` | `sensor_msgs/PointCloud2` | `x, y, z, intensity` (float32) |
| `/tf_static` | `tf2_msgs/TFMessage` | Sensor extrinsics, latched |
| `/tf` | `tf2_msgs/TFMessage` | Ego pose `map → base_link` from OXTS (optional) |
| `/kitti/imu` | `sensor_msgs/Imu` | Orientation, angular velocity, linear acceleration (optional) |

### Transform tree

```
map ──(/tf, per frame)──▶ base_link ──(/tf_static)──┬──▶ velodyne
                                                     └──▶ camera_*
```

`base_link` is coincident with the OXTS/IMU unit. Static extrinsics are
expressed relative to `base_link` so the dynamic ego pose has a single,
conflict-free child to drive — a valid single-parent tree.

---

## How it works

- **No ROS dependency.** `pykitti` reads the data; `mcap` + `mcap-ros2-support`
  write the file. All ROS2 message schemas are emitted from inline `.msg`
  definitions ([`ros2_defs.py`](kitti2mcap/ros2_defs.py)), so the MCAP is fully
  self-describing.
- **Schemas registered up front**, so the file is readable without a ROS
  environment.
- **Lazy, prefix frame loading** — one frame at a time, so long drives don't
  exhaust RAM.
- **Faithful timestamps.** Each sensor's `timestamps.txt` is parsed directly to
  **nanosecond** precision (pykitti truncates to microseconds), so per-sensor
  timing and playback rate are preserved.
- **Rectified camera model.** KITTI sync images are already rectified, so
  `CameraInfo` uses `P_rect`/`R_rect`/`K` with zero distortion.
- **`/tf_static` latched** at the first frame's timestamp so the transform tree
  is available immediately on load.

---

## Project structure

```
kitti2mcap/
├── cli.py            # CLI entrypoint, arg parsing, orchestration
├── reader.py         # Wraps pykitti; per-sensor nanosecond timestamps
├── calibration.py    # pykitti calib → CameraInfo + static transform tree
├── ros2_defs.py      # Concatenated ROS2 .msg schema text (self-contained)
├── mcap_writer.py    # Wraps mcap-ros2-support; schema registration + writing
└── writers/
    ├── image.py      # PIL image → sensor_msgs/Image
    ├── pointcloud.py # velodyne .bin → sensor_msgs/PointCloud2
    ├── imu.py        # OXTS packet → sensor_msgs/Imu
    └── tf.py         # quaternion/transform helpers, TFMessage builders
```

---

## Inspecting the output

With the [`mcap` CLI](https://github.com/foxglove/mcap):

```bash
mcap info out.mcap
mcap doctor out.mcap
```

Or trim a slice for a quick test:

```bash
mcap filter out.mcap tiny.mcap --start-secs 0 --end-secs 1
```
