Adding Datasets¶
This tutorial walks through adding a new source dataset to the 123D conversion pipeline. A dataset is integrated by implementing a set of parser classes that read the raw data and yield 123D’s unified datatypes. The framework then handles configurable synchronization, log writing, and exposing the result through the unified API.
1. General¶
Architecture overview¶
Conversion is organized around three base classes defined in
BaseDatasetParser. A top-level
dataset parser produces lightweight, per-log and per-map handles that .
The orchestrator constructs these once on the main process, then distributes them
to parallel workers, which perform the heavy I/O internally and hand the parsed data to the
log/map writers:
BaseDatasetParser
├── get_log_parsers() ──▶ [ LogParser_1, ..., LogParser_N ] ──▶ ArrowLogWriter
└── get_map_parsers() ──▶ [ MapParser_1, ..., MapParser_M ] ──▶ ArrowMapWriter
│
▼
Arrow storage ──▶ unified API
Each new dataset adds a package under src/py123d/parser/<dataset>/. Existing
packages (i.e., nuscenes/, nuplan/, wod/, av2/, kitti360/,
pandaset/) are implemented as references.
The dataset parser¶
Subclass BaseDatasetParser and
implement two methods:
from py123d.parser.base_dataset_parser import BaseDatasetParser
class MyDatasetParser(BaseDatasetParser):
def get_log_parsers(self) -> list[BaseLogParser]:
"""One lightweight, picklable handle per log/scene."""
...
def get_map_parsers(self) -> list[BaseMapParser]:
"""One handle per map region (return [] if the dataset has no maps)."""
...
Important
The returned handles are shipped to worker processes. Keep them lightweight, i.e., store only paths and parameters, not open file handles, database connections, or decoded sensor data. All expensive I/O belongs inside the iterators (covered below), which run on the worker.
Optional and gated dependencies¶
Dataset-specific third-party packages (devkits, format readers, cloud SDKs) should
not be hard dependencies of 123D. Declare them as an extra in
pyproject.toml under [project.optional-dependencies]:
[project.optional-dependencies]
mydataset = ["mydataset-devkit==1.2.3"]
Then gate the import at the top of your parser (or lazily inside the method that
needs it) with check_dependencies(), which
raises a helpful, install-instruction error if the package is missing:
from py123d.common.utils.dependencies import check_dependencies
check_dependencies(["mydataset"], optional_name="mydataset")
# ImportError: Missing 'mydataset'. Install with: pip install py123d[mydataset]
Entry point¶
Conversion is driven by the py123d-conversion CLI (defined in
[project.scripts], mapping to
py123d.script.run_conversion.main()). It uses Hydra to instantiate your parser from a YAML config via its
_target_ field (see section 5):
py123d-conversion dataset=mydataset
2. Log Parser¶
A log parser is a handle to a single continuous driving sequence (i.e., called ‘log’ in 123D). Subclass
BaseLogParser and implement the log
metadata getter and the synchronized-frame iterator.
Log metadata¶
get_log_metadata() returns a
LogMetadata describing the log:
Field |
Description |
|---|---|
|
Dataset name, lowercase and dashed (e.g. |
|
Data split, prefixed by dataset (e.g. |
|
Unique name of the log. |
|
Geographic location (optional); links a log to a per-location map. |
|
Associated |
|
123D version that produced the metadata (defaults to the current version). |
Iterating modalities¶
iter_modalities_sync() lazily yields one
ModalitiesSync per synchronized frame.
Each frame bundles a timestamp with a list of 123D modalities. The unified
BaseModality types parsed from
the dataset’s native formats (ego state, bounding boxes, cameras, lidar, radar,
…):
from py123d.parser.base_dataset_parser import ModalitiesSync
def iter_modalities_sync(self):
for frame in self._read_frames():
modalities = [
self._parse_ego_state(frame),
self._parse_boxes(frame),
*self._parse_cameras(frame),
self._parse_lidar(frame),
]
yield ModalitiesSync(timestamp=frame.timestamp, modalities=modalities)
Parsed sensor datatypes and lazy I/O¶
Sensor data is the bulk of any dataset, so 123D may optionally skip decoding it during parsing. Instead, you can pass a parsed modality helpers, which carry only a file path (or raw bytes) plus metadata. Depending on the storage option for sensor data, the writer can just store the sensor reference, or decode and re-compress the data internally for self-contained storage.
ParsedCamera: camera metadata, acamera_to_global_se3pose, and either adataset_root/relative_pathpair or an in-memorybyte_string.ParsedLidar: lidar metadata,start_timestamp/end_timestamp(the sweep window), adataset_root/relative_path, and optionalload_kwargs.ParsedRadar: radar metadata, a singletimestamp(a radar scan is an instantaneous snapshot), a file path, and optionalload_kwargs.
If sensor storage is configured as “path”, the whole conversion process is substantially faster, as the conversion is not memory- or I/O-bound. On the other hand, the converted logs are not self-contained and require access to the original dataset files for loading.
Tip
load_kwargs is a generic, dataset-specific dictionary forwarded verbatim to
the point-cloud loader and persisted in the Arrow frame under the "path"
store option. nuScenes uses it to carry the keyframe’s panoptic-label path
alongside the lidar file.
Where to add sensor I/O¶
If your dataset uses a point-cloud or image format not already supported, add a
loader under src/py123d/common/io/{camera,lidar,radar}/ and add it in the
dispatcher
py123d.common.io.lidar.path_lidar_io.load_point_cloud_data_from_path()
(i.e. for lidar). The dispatcher routes by file extension / store option and is where
load_kwargs is consumed.
Sync vs. async parsing¶
For simplicity, the default configuration writes all modalities as synchronized frames.
Dataset with asynchronous modalities can be implemented by overriding the async iterator
iter_modalities_async(),
which yields each modality independently as it is produced. The default
implementation simply unwraps the synchronized frames, so overriding is optional.
Async writing is selected by the async_conversion flag in the config
(section 5). Importantly, an individual modality must be parsed in
increasing timestamp order, but modalities can be interleaved in any way.
3. Map Parser¶
Maps are usually the trickiest part to unify since HD-map schemas vary widely across datasets.
Maps are optional: if your dataset ships none, return [] from get_map_parsers() and skip this section.
We generally recommend starting with log parsing and adding maps later.
A map parser is a handle to one map region. Subclass
BaseMapParser and implement:
from py123d.parser.base_dataset_parser import BaseMapParser
class MyMapParser(BaseMapParser):
def get_map_metadata(self) -> MapMetadata:
...
def iter_map_objects(self):
"""Yield map objects lazily, one at a time."""
yield from self._parse_lanes()
yield from self._parse_crosswalks()
# ...
Since map elements are static datatypes, they have no timestamp information and are expected to be expressed in the logs global coordinate frame.
Map metadata¶
get_map_metadata() returns a
MapMetadata. Two fields deserve
particular attention:
map_has_z: whether the map geometry carries elevation (3D) or is 2D only.map_is_per_log: whether each log has its own map (i.e., as in Waymo datasets) or maps are shared per location across logs (e.g., for each city in nuScenes/nuPlan).
Supported map elements¶
iter_map_objects() yields
BaseMapObject instances.
Each object is one of the supported element types (the
MapLayer enum), defined in
src/py123d/datatypes/map_objects/map_objects.py:
Element |
Geometry |
Notes |
|---|---|---|
|
surface |
Individual driving lane with boundaries and centerline. Lane connectors are
also yielded as |
|
surface |
Group of lanes travelling in the same direction. |
|
surface |
Intersection area. |
|
surface |
Pedestrian crossing. |
|
surface |
Pedestrian walkway. |
|
surface |
Parking area. |
|
surface |
Other drivable surface. |
|
surface |
Zone governed by a stop sign / light. |
|
line |
Road boundary. |
|
line |
Painted lane marking. |
|
surface |
Speed-reduction area. |
Surface elements derive from BaseMapSurfaceObject (a polygon outline), and
line elements derive from BaseMapLineObject (a polyline).
Note
If a map element type isn’t listed above, then new layers can be added. Please open an issue or PR (see Contributing) so the schema stays consistent across datasets.
4. Downloader (optional)¶
A downloader is optional, but it can significantly lower the onboarding barrier: A dataset or subset can be downloaded directly from the 123D CLI. The existing implementations in 123D also show examples how to download and convert in one script (i.e. using the streaming during conversion approach described below).
Subclass BaseDownloader, placing it at
src/py123d/parser/<dataset>/<dataset>_download.py:
from py123d.parser.base_downloader import BaseDownloader
class MyDatasetDownloader(BaseDownloader):
def download(self) -> None:
"""Fetch the selected data into ``self.output_dir``."""
...
The base class provides output_dir (destination; None means a temp
directory is assigned at runtime) and dry_run (log the plan without writing).
Integration follows two paths:
Standalone CLI:
py123d-download dataset=mydatasetinstantiates the downloader and callsdownload().Streaming during conversion: the parser accepts an optional
downloader=argument, redirects itsoutput_dirto a session-scoped temp directory, downloads before reading, and cleans up on destruction.
5. Hydra config¶
Dataset Paths¶
123D centralizes the dataset roots that parsers read from in a single Hydra
config group at src/py123d/script/config/common/default_dataset_paths.yaml,
typed by the DatasetPaths
dataclass in src/py123d/common/runtime/dataset_paths.py. For a new dataset,
add a mydataset_data_root: ${oc.env:MYDATASET_DATA_ROOT,null} entry to the
YAML.
By default, we use a environment variable (e.g. MYDATASET_DATA_ROOT) to avoid hardcoding local paths in the config.
In your dataset config (see below), you can reference the path to pass it to the log/map parsers.
Dataset Parsers¶
Conversion configs live in
src/py123d/script/config/conversion/dataset/, one <name>.yaml per dataset
variant. The base defaults are in default_conversion.yaml. Each dataset config
has four sections:
parser: instantiates yourBaseDatasetParser(via_target_) and its data-root paths / options.log_writer_config: storage options specific to the arrow log writer, i.e. configures sensor storage (camera_store_option,lidar_store_option,lidar_codec), theasync_conversionflag, and inference toggles.log_writer— theArrowLogWriter, including async_config(thereference_columnanddirectionused to snap modalities onto a common timeline).map_writer— theArrowMapWriter.
Provide a local config and, if you wrote a downloader, a separate stream config. Keep them as distinct files as they differ in data roots and storage:
Data is pre-downloaded; roots point at it and sensors are stored as paths.
parser:
_target_: py123d.parser.mydataset.mydataset_parser.MyDatasetParser
_convert_: 'all'
splits: ["mydataset_train", "mydataset_val"]
mydataset_data_root: ${dataset_paths.mydataset_data_root}
log_writer_config:
_target_: py123d.api.scene.arrow.utils.log_writer_config.LogWriterConfig
_convert_: 'all'
camera_store_option: "path" # "path", "jpeg_binary", "png_binary"
lidar_store_option: "path" # "path", "binary"
lidar_codec: null
async_conversion: ${async_conversion}
# ...
Roots are null (auto-detected in a temp dir); a downloader block is
added and sensors are stored as binary (temp files vanish after
conversion).
parser:
_target_: py123d.parser.mydataset.mydataset_parser.MyDatasetParser
_convert_: 'all'
splits: ["mydataset_train", "mydataset_val"]
mydataset_data_root: null # auto-detected
downloader:
_target_: py123d.parser.mydataset.mydataset_download.MyDatasetDownloader
_convert_: 'all'
output_dir: null # temp dir assigned at runtime
splits: ${dataset.parser.splits}
dry_run: false
log_writer_config:
# ...
camera_store_option: "jpeg_binary"
lidar_store_option: "binary"
lidar_codec: "laz"
6. Documentation¶
Finally, document the dataset so users know how to obtain, install, and convert it:
Add a page
docs/datasets/<name>.rst: You can mirror an existing one. The established structure is: an Overview dropdown (paper, download, license, splits), an Available Modalities table, Download, Installation (the pip extra), Conversion (local + streaming), any dataset-specific sections, Dataset Issues, and Citation.Register the page in the
docs/datasets/index.rsttoctree.Build the docs locally and confirm the page renders without warnings before opening your PR.
Checklist¶
Step |
Location |
|---|---|
|
|
|
same package |
|
same package |
Sensor I/O loader (if new format) |
|
Optional dependency extra + gating |
|
Downloader (optional) |
|
Hydra config pair |
|
Dataset doc page + index entry |
|
Thank you for contributing a dataset to 123D! If anything in this guide is unclear or you hit a case it doesn’t cover, please open an issue or PR.