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.
open_source Helper (Recommended)¶
The easiest and most common way to get a FrameSetSource is to use the open_source helper function. This function provides a unified interface that automatically detects your data source and instantiates the correct FrameSetSource implementation for you.
It can handle:
Live sensors (by hostname or IP)
.pcap files (with an associated .json metadata file)
.osf files
.bag and .mcap files (Python only)
See open_source Parameters in open_source & open_packet_source for a full description of its usage and parameters.
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.