C++ Examples

To facilitate working with the Ouster C++ SDK, we provide these examples of common operations. The examples explained below are compiled into executables which print to screen to demonstrate behavior. Build with BUILD_EXAMPLES and print to screen to demonstrate behavior.

Sensor Configuration

In this example we show various ways to work with the sensor configuration interface. The ouster::sensor::sensor_config struct can be found defined in types.h. get_config and set_config are in client.h.

To run this example:

config_example $SENSOR_HOSTNAME

If there are no errors, you should see a printout in five parts.

Let’s look at the first step, where we get the configuration of the sensor as it starts:

sensor::sensor_config original_config;
if (!sensor::get_config(sensor_hostname, original_config)) {
    std::cerr << "..error: could not connect to sensor!" << std::endl;
    return EXIT_FAILURE;
}

The function get_config takes a sensor_config so we must declare it first. It returns true if it successfully retrieves the config, and false if an error occurs in connecting to the sensor and setting the config. The function set_config works similarly, returning true if it successfully sets the config, and false otherwise.

In the second step, we create a new config:

sensor::sensor_config config;
config.azimuth_window = std::make_pair<int>(90000, 270000);
config.lidar_mode = sensor::lidar_mode::MODE_512x10;

// If relevant, use config_flag to set udp dest automatically
uint8_t config_flags = 0;
const bool udp_dest_auto = true;  // whether or not to use auto destination
const bool persist =
    false;  // whether or not we will persist the settings on the sensor

if (udp_dest_auto) config_flags |= ouster::sensor::CONFIG_UDP_DEST_AUTO;
if (persist) config_flags |= ouster::sensor::CONFIG_PERSIST;

The sensor_config struct consists of several std::optional members, which can be set directly. Members which are not set will not set sensor configuration parameters when sent to the sensor. In addition, there are two config flags, for automatic udp destination setting and config persistence which can be set as above. The automatic udp destination flag cannot be set when config.udp_dest is set, as those conflict.

Working with LidarScans

The ouster::LidarScan is explained in depth conceptually in the LidarScan reference. Here we cover some specifics that will be useful for C++ developers.

LidarScan constructors

Of foremost interest when using a LidarScan is constructing a LidarScan which suits your needs.

The simplest (and most common) method is to construct one to contain all data coming from your sensor. You can do this by specifying the udp_proflie_lidar in your sensor_info, or by directly specifying the profile you care about.

For example, see the two snippets below:

auto profile_scan = ouster::LidarScan(w, h, info.format.udp_profile_lidar);
auto dual_returns_scan = ouster::LidarScan(
    w, h, UDPProfileLidar::PROFILE_RNG19_RFL8_SIG16_NIR16_DUAL);

But suppose you don’t care about some of the data, such as the ambient and signal fields. You can also specify to your LidarScan to only batch the relevant fields like so:

// Finally, you can construct by specifying fields directly
static const std::array<ouster::FieldType, 2> reduced_slots{
    {{ChanField::RANGE, ChanFieldType::UINT32},
     {ChanField::NEAR_IR, ChanFieldType::UINT16}}};
auto reduced_fields_scan =
    ouster::LidarScan(w, h, reduced_slots.begin(), reduced_slots.end());

To see this in action, you can run the example executable lidar_scan_example:

$ lidar_scan_example $SAMPLE_DUAL_RETURNS_PCAP $SAMPLE_DUAL_RETURNS_JSON

The source code of lidar_scan_example is available here.

2D Representations and 3D representations

The core destaggering and projection to 3D capabilities are demonstrated in the representations_example executable.

Here we will cover slightly more sophisticated ways of working with the data also demonstrated in that example.

To run this example:

representations_example $SAMPLE_DUAL_RETURNS_PCAP $SAMPLE_DUAL_RETURNS_JSON

The source code of representations_example is available on the github.

Reshaping XYZ to 2D

Users may find that they wish to access the x, y, and z coordinates of a single return in a similar way. As the conversiton to cartesian coordinates returns an Eigen::Array n x 3, with n = w * h, reshaping the resulting array is necessary.

We can combine our knowledge in projecting into cartesian coordinates and destaggering using the following function:

img_t<double> get_x_in_image_form(const LidarScan& scan, bool destaggered,
                                  const sensor::sensor_info& info) {
    // For convenience, save w and h to variables
    const size_t w = info.format.columns_per_frame;
    const size_t h = info.format.pixels_per_column;

    // Get the XYZ in ouster::Points (n x 3 Eigen array) form
    XYZLut lut = make_xyz_lut(info);
    auto cloud = cartesian(scan.field(sensor::ChanField::RANGE), lut);

    // Access x and reshape as needed
    // Note that the values in cloud.col(0) are ordered
    auto x = Eigen::Map<const img_t<double>>(cloud.col(0).data(), h, w);
    auto x_destaggered = destagger<double>(x, info.format.pixel_shift_by_row);

    // Apply destagger if desired
    if (!destaggered) return x;
    return x_destaggered;
}

This demonstrates the functionality with x, but it can be easily expanded to cover y and z as well.

Adjusting XYZLut With External Matrix

Users may find that they wish to apply an extra transform while projecting to cartesian coordinates. Such a transform, likely an extrinsics matrix of some sort, can be baked into the ouster::XYZLut created with ouster::make_xyz_lut().

In the following code, transformation represents the extrinsincs transform:

auto lut_extrinsics = make_xyz_lut(
    w, h, sensor::range_unit, info.beam_to_lidar_transform, transformation,
    info.beam_azimuth_angles, info.beam_altitude_angles);

std::cerr << "Calculating 3d Points of with special transform provided.."
          << std::endl;
auto cloud_adjusted = cartesian(range, lut_extrinsics);

Reading Scans From An OSF File

The OSF file is a common format used to store Ouster sensor data. It can be useful to read the file outside of the ouster-cli utility in order to perform more advanced processing.

Below you can see an example which reads each scan in an OSF and prints them to stdout:

#include <iostream>

#include "ouster/impl/build.h"
#include "ouster/osf/reader.h"
#include "ouster/osf/stream_lidar_scan.h"

using namespace ouster;

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Version: " << ouster::SDK_VERSION_FULL << " ("
                  << ouster::BUILD_SYSTEM << ")"
                  << "\n\nUsage: osf_reader_example <osf_file>" << std::endl;

        return (argc == 1) ? EXIT_SUCCESS : EXIT_FAILURE;
    }

    const std::string osf_file = argv[1];

    // open OSF file
    osf::Reader reader(osf_file);

    // read all messages from OSF in timestamp order
    for (const auto& m : reader.messages()) {
        std::cout << "m.ts: " << m.ts().count() << ", m.id: " << m.id()
                  << std::endl;

        // In OSF file there maybe different type of messages stored, so here we
        // only interested in LidarScan messages
        if (m.is<osf::LidarScanStream>()) {
            // Decoding LidarScan messages
            auto ls = m.decode_msg<osf::LidarScanStream>();

            // if decoded successfully just print on the screen LidarScan
            if (ls) {
                std::cout << "ls = " << to_string(*ls) << std::endl;
            }
        }
    }
}

Writing Scans To An OSF File

An API for writing to the OSF file format is also exposed. This is most often used for writing scans and metadata, possibly with a reduced number of fields in order to save data.

Below you can see an example which creates a scan and writes it to an OSF File using the Writer API: