FrameSetSource

Data consumption in the Ouster SDK is built on two core abstractions:

  • PacketSource: A low-level iterator that yields raw LidarPacket and ImuPacket objects as they are read from a sensor or file.

  • FrameSetSource: A high-level iterator that consumes a PacketSource, batches the packets into complete frames, and yields FrameSet objects.

By default, a FrameSet is a container (like a list) that holds one slot per sensor in the source for a given time slice. Each slot may hold a LidarFrame for that sensor, but depending on collation a slot may be empty if no frame was available for that sensor in the given time slice. For a single sensor source, the FrameSet will simply contain one LidarFrame object.

Note

How a FrameSet is populated depends on whether the source is collated. By default (collate=True), each FrameSet holds one LidarFrame per sensor for a given time slice. An uncollated source instead yields a FrameSet containing a single frame from a single sensor per iteration, cycling between sensors. See FrameSetSource Collation for details.

For almost all applications, you should use a FrameSetSource.

Obtaining Sensor Info

Ouster sensors require sensor info to interpret the readings of the sensor. Represented by the object SensorInfo, metadata fields include configuration parameters such as lidar_mode and sensor intrinsics like beam_azimuth_angles.

When you work with a sensor, the client will automatically fetch the metadata. Note that, recorded pcaps, however, must always be accompanied by a json file containing the metadata of the sensor as it was when the data was recorded.

Every FrameSetSource holds a reference to the sensor metadata, which has crucial information that is important when processing the individual frames. A user can access the metadata through the sensor_info property of a FrameSetSource object:

with closing(sensor.SensorFrameSetSource(sensor_hostname)) as source:
    sensor_info = source.sensor_info[0]
    print("Retrieved sensor info:\n")
    print(f"  serial no:        {sensor_info.sn}")
    print(f"  firmware version: {sensor_info.fw_rev}")
    print(f"  product line:     {sensor_info.prod_line}")
    print(f"  lidar mode:       {sensor_info.config.lidar_mode}")
    print(f"  columns/frame:    {sensor_info.format.columns_per_frame}")
    print(f"  beam angles:      {len(sensor_info.beam_altitude_angles)}")
    print(f"  altitude:         {len(sensor_info.beam_azimuth_angles)}")
    print(f"Writing to: {sensor_hostname}.json")
    with open(f"{sensor_hostname}.json", "w") as f:
        f.write(sensor_info.to_json_string())
sensor::SensorFrameSetSource source(sensor_hostname);
auto sensor_info = source.sensor_info()[0];
std::cout << "Retrieved sensor info:\n";
std::cout << "  serial no:        " << sensor_info->sn << "\n";
std::cout << "  firmware version: " << sensor_info->fw_rev << "\n";
std::cout << "  product line:     " << sensor_info->prod_line << "\n";
std::cout << "  lidar mode:       " << core::to_string(sensor_info->config.lidar_mode.value()) << "\n";
std::cout << "  columns/frame:    " << sensor_info->format.columns_per_frame << "\n";
std::cout << "  beam angles:      " << sensor_info->beam_altitude_angles.size() << "\n";
std::cout << "  altitude:         " << sensor_info->beam_azimuth_angles.size() << "\n";
std::cout << "Writing to " << sensor_hostname << ".json\n";
std::ofstream out(sensor_hostname + ".json");
out << sensor_info->to_json_string();

Note

The sensor_info property returns a list of SensorInfo objects, even for single-sensor sources. This maintains consistency between single and multi-sensor data sources. Most visualization functions accept this list directly, supporting both single and multi-sensor sources automatically. Use [0] to access the first sensor’s metadata.

Specific FrameSetSource Implementations

While open_source is best for most cases, you can instantiate a specific FrameSetSource class if you need granular control over its parameters.

SensorFrameSetSource

Use SensorFrameSetSource FrameSetSource to connect directly to one or more live sensors. It manages the network connection, fetches metadata, and batches incoming UDP packets into frames.

It owns the packet capture thread, collates UDP packets into LidarFrame frames, and yields one FrameSet per iteration.

When to use it

  • You need explicit control over queue depth, per-sensor timeouts, soft-ID validation, or field filtering.

  • You want completed frames rather than packets, but still need low latency for online processing.

First, let’s add the necessary imports to work with the API.

# Other imports
from ouster.sdk import core
from ouster.sdk import sensor
#include "ouster/core/types.h"
#include "ouster/sensor/client.h"
using namespace ouster::sdk;

Single sensor example

source = sensor.SensorFrameSetSource(hostname)
sensor::SensorFrameSetSource source(hostname);

Multiple sensors example

# Replace with actual hostnames
# hosts = ["os-xxxx.local", "os-yyyy.local"]
source = sensor.SensorFrameSetSource(list(hosts))
// Replace with actual hostnames
// std::vector<std::string> hostnames{"os-xxxx.local", "os-yyyy.local"};
sensor::SensorFrameSetSource source(hostnames);

PcapFrameSetSource

Use PcapFrameSetSource to read from a .pcap file and automatically batch it into complete frames. This is the simplest, high-level approach if your goal is to work with LidarFrame objects.

A .pcap file only contains raw network packets. To interpret this data, PcapFrameSetSource must be provided with the sensor’s .json metadata file via the meta parameter.

Internally, PcapFrameSetSource builds upon the packet-level tools described in Low-Level Access below.

Imports

from ouster.sdk import pcap
#include "ouster/pcap/pcap_frame_set_source.h"

Using PcapFrameSetSource to read source

# For additional options see API documentation for PcapFrameSetSource
source = pcap.PcapFrameSetSource(pcap_path,
                             meta=[metadata_path])
pcap::PcapFrameSetSourceOptions options;
options.meta = {metadata_path};
auto source = pcap::PcapFrameSetSource(pcap_path, options);

OsfFrameSetSource

Use OsfFrameSetSource to read from an .osf file. OSF (Open Sensor Format) is a structured format that conveniently stores the sensor metadata directly within the file, so no separate .json file is needed.

Imports

# Other imports
import ouster.sdk.osf as osf
#include "ouster/osf/reader.h"
using namespace ouster::sdk;

Using OsfFrameSetSource to read source

source = osf.OsfFrameSetSource(osf_file)
count = 0

for frame_set in source:
    for frame in frame_set:
        if frame is None:
            continue
        print(f'frame = {frame},'
              f' WxH={frame.w}x{frame.h}')
        count += 1
        if count >= limit:
            return
source.close()
auto source = osf::OsfFrameSetSource(osf_path);
std::size_t count = 0;

for (const auto& frame_set : source) {
    for (const auto& frame : frame_set) {
        if (!frame) { 
            continue; }
        std::cout << "frame = " << to_string(*frame)
                  << ", WxH=" << frame->w << "x" << frame->h << '\n';
        if (++count >= limit) {
            return;
} } }

This FrameSetSource implementation uses an osf.Reader internally to read and decode frame messages from the file.

BagFrameSetSource (Python Only)

Use BagFrameSetSource to read ROS .bag or .mcap files. This Python-only class wraps BagPacketSource, which reads ROS messages from the bag and converts them into LidarPacket objects. It then batches these packets into FrameSet objects.

Metadata can be provided via the meta parameter or, in many cases, automatically detected from metadata topics within the bag file.

Low-Level Access

Below the FrameSetSource abstraction, the SDK exposes lower-level tools for working directly with OSF messages and raw PCAP packets. Reach for these only when the high-level sources above do not give you the control you need.

osf.Reader

You may notice the osf.Reader class. It’s important to understand that osf.Reader is not a FrameSetSource.

  • OsfFrameSetSource (High-Level): An iterator that automatically finds lidar streams, decodes messages, and yields FrameSet objects. Use this for reading frames.

  • osf.Reader (Low-Level): The base Reader interface that gets info about start/end_ts, reads and decodes all metadata entries, get access to chunks and messages of the OSF file but it does not automatically decode them into frames.

When to use osf.Reader directly:

  • Read raw messages or custom metadata from an OSF file.

  • Inspect the file’s sensor info store without reading all frames.

  • Analyze the file’s low-level chunk structure or statistics.

A common use for osf.Reader is to quickly access the sensor info stored in the file without iterating through the frames.

Get Sensors Info with osf.Reader

Sensors information is stored as osf.LidarSensor metadata entry and can be read with the reader.meta_store.find() function that returns all metadata entry of the specified type (in our case it’s of type osf.LidarSensor):

reader = osf.Reader(osf_file)
lidar_metadata = reader.meta_store.find(osf.LidarSensor)
if not lidar_metadata:
    print("No LidarSensor metadata entries found.")
    return
for meta_id, lidar_entry in lidar_metadata.items():
    lidar_sensor = cast(osf.LidarSensor, lidar_entry)
    info = lidar_sensor.info
    print(f"meta[{meta_id}] -> sn={info.sn}"
          f", fw_rev={info.fw_rev}, "
          f", prod_line={info.prod_line}")
osf::Reader metadata_reader(osf_file);
const auto lidar_metadata = metadata_reader.meta_store().find<osf::LidarSensor>();
if (lidar_metadata.empty()) {
    std::cout << "No LidarSensor metadata entries found." << std::endl;
} else {
    for (const auto& entry : lidar_metadata) {
        const auto meta_id = entry.first;
        const auto& info = entry.second->info();
        std::cout << "meta[" << meta_id << "] -> sn=" << info.sn
                  << ", fw_rev=" << info.fw_rev
                  << ", prod_line=" << info.prod_line << std::endl; } }

Reading frames with osf.Reader

The code below demonstrates reading LidarFrame from an OSF file using the low-level osf.Reader class.

reader = osf.Reader(osf_file)
# Read all messages from OSF in timestamp order
for message in reader.messages():
    print(f"message.ts: {message.ts}"
          f", message.id: {message.id}")
    # In OSF file there maybe different type of messages stored, so here we
    # only interested in LidarFrame messages
    if message.of(osf.LidarFrameStream):
        # Decoding LidarFrame messages
        lidar_frame = message.decode()
        # if decoded successfully just print on the screen LidarFrame
        if lidar_frame is not None:
            print(f"ls = {lidar_frame}")
osf::Reader reader(osf_file);
// Read all messages from OSF in timestamp order
for (const auto& message : reader.messages()) {
    std::cout << "message.ts: " << message.ts().count()
              << ", message.id: " << message.id() << std::endl;
    // In OSF file there maybe different type of messages stored.
    // We must check the type of message before decoding it.
    if (message.is<osf::LidarFrameStream>()) {
        // Decoding LidarFrame messages
        auto lidar_frame = message.decode_msg<osf::LidarFrameStream>();
        // if decoded successfully just print on the screen LidarFrame
        if (lidar_frame) {
            std::cout << "ls = " << to_string(*lidar_frame) << std::endl;
        }
    } else if (message.is<osf::FrameSetSourceMetadataStream>()) {
        // Decoding FrameSetSourceMetadataStream messages
        auto frame_set_source_meta =
            message.decode_msg<osf::FrameSetSourceMetadataStream>();

        // if decoded successfully just print on the screen
        // FrameSetSourceMetadata
        if (frame_set_source_meta) {
            std::cout << "Found metadata collection with keys:\n";
            auto keys = frame_set_source_meta->keys();
            for (const auto& key : keys) {
                std::cout << "  key: " << key << std::endl;
            }
        }

Pcap Packet Replay

If you need to operate on individual packets rather than full frames, you can use the SDK’s packet-level tools. This is useful for experimenting with the SDK APIs without a live sensor or for building custom processing pipelines.

Use PcapPacketSource to replay packets from a PCAP file. It yields fully typed LidarPacket and ImuPacket objects, with each item carrying the originating sensor index alongside the packet itself.

Example: iterate over typed packets from a PCAP file.

source = pcap.PcapPacketSource(str(pcap_path))
metadata = source.sensor_info[0]
packet_format = core.PacketFormat(metadata)

for sensor_idx, packet in source:
    if isinstance(packet, core.LidarPacket):
        frame_id = packet_format.frame_id(packet.buf)
        print(f"sensor={sensor_idx} lidar frame_id={frame_id}"
              f" bytes={len(packet.buf)}")
        counts["lidar"] += 1
    elif isinstance(packet, core.ImuPacket):
        print(f"sensor={sensor_idx} imu"
              f" host_ts={packet.host_timestamp}")
        counts["imu"] += 1
source.close()
pcap::PcapPacketSource source(pcap_file);

for (const auto& item : source) {
    const int sensor_idx = item.first;
    const auto& packet_ptr = item.second;
    if (!packet_ptr) continue;
    if (packet_ptr->type() == core::PacketType::Lidar) {
        std::cout << "sensor=" << sensor_idx << " lidar frame_id=" << packet_ptr->frame_id()
                  << " bytes=" << packet_ptr->buf.size() << '\n';
        ++counts.lidar;
    } else if (packet_ptr->type() == core::PacketType::Imu) {
        std::cout << "sensor=" << sensor_idx << " imu host_ts=" << packet_ptr->host_timestamp
                  << '\n';
        ++counts.imu;
    }
}

Refer to the Ouster SDK on GitHub codebase or the Python API reference for details beyond the high-level API.