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 conversion 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
// Do not apply extrinsics in this case (the false)
XYZLut lut = make_xyz_lut(info, false);
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:
#include <iostream>
#include "ouster/impl/build.h"
#include "ouster/osf/writer.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_writer_example <osf_file>" << std::endl;
return (argc == 1) ? EXIT_SUCCESS : EXIT_FAILURE;
}
const std::string osf_file = argv[1];
// Start writing a 1 stream OSF file with a default initialized sensor info
osf::Writer writer(
osf_file, sensor::default_sensor_info(ouster::sensor::MODE_512x10));
// Instantiate a lidar scan with the expected width and height
// default_sensor_info assumes a 64 plane sensor
LidarScan scan(512, 64);
// Manipulate the scan as desired here
// Write it to file on stream 0
writer.save(0, scan);
}