Using the API¶
New types and functions¶
The Ouster Perception API introduces the following types and functions:
Object — a generic object, represented by a bounding box,
object_to_bodyandbody_to_worldposes, velocity, classification, a timestamp, and user-definable set of properties represented by strings.DetectionConfig — base configuration for detection engines. Subclasses such as ClassicDetectionConfig provide settings for the classic cluster-based engine.
DetectionEngine — a base class that declares virtual methods for modifying (e.g. by adding objects to) LidarFrame and FrameSet.
ClassMap — A mapping from integers to strings, meant to map
Object::class_idto a string description.ClassMapSet — A mapping from strings to
ClassMapinstances, allowing users to specify different descriptions forObjectinstances derived from differentDetectionEngine.FrameSetSourceMetadataSet — A container for instances of
ClassMapSetand other metadata a user may want to associate with aFrameSetSource. Currently, because OSF is the only format that supportsLidarFrameandFrameSet, the OsfFrameSetSource is the only implementation that supports metadata.
Using DetectionEngine¶
To use the API, call DetectionEngine.create to
instantiate the engine, passing the SensorInfo list (even for a single sensor) and an optional
ClassicDetectionConfig (defaults to ClassicDetectionConfig{}).
Then call
update
method, providing either a LidarFrame or FrameSet by reference. The following example describes how to instantiate
the default DetectionEngine and use it to add objects representing lidar point clusters to every frame in the first
sensor stream in the provided source.
# Open a frame source
source = open_source(source_url)
# Create a detection engine
engine = perception.DetectionEngine.create(source.sensor_info)
# Iterate over the frame sets in the frame source
frame_counter = 0
for frame_set in source:
frame = frame_set[0]
# Apply the engine to the frame (or the whole frame set.)
engine.update(frame)
# Inspect the objects added by the engine.
print(f'Frame: {frame_counter}')
frame_counter += 1
for key, objects in frame.objects.items():
print(f'Key {key} has {len(objects)} objects.')
// Open a frame set source
auto source = open_source(source_url);
// Create a detection engine
auto engine = perception::DetectionEngine::create(source.sensor_info());
// Iterate over the frame sets in the frame set source
int frame_counter{0};
for (auto frame_set : source) {
auto& frame = *frame_set[0];
// Apply the engine to the frame (or the whole frame set.)
engine->update(frame);
// Inspect the objects added by the engine.
std::cerr << "Frame: " << frame_counter << '\n';
frame_counter++;
for (const auto& kv : frame.objects()) {
std::cerr << "Key " << kv.first << " has " << kv.second.size() << " objects.\n";
}
}
Adding Object to frames without a DetectionEngine¶
While the DetectionEngine interface is the most straightforward way to add objects to frames, users can also add
objects directly to a frame without using a DetectionEngine. This may be useful for users who want to add objects
derived from an external source, such as an annotated dataset, or who want to implement their own detection engine
without inheriting from the base class.
To add an Object to a frame, simply create an instance of the Object class and add it to the frame’s objects
vector.For example, the following code snippet creates an Object with poses, velocity, and class ID, and adds
it to a frame:
from ouster.sdk import core
import numpy as np
objects = [core.Object(), core.Object()]
objects[0].id = 1
objects[0].creation_ts = 99
objects[0].timestamp = 199
objects[0].class_id = 1
objects[0].class_confidence = 0.9
objects[0].object_to_body.position = np.array([1, 2, 3])
objects[0].object_to_body.set_rotation(np.array([2, 2, 2]))
objects[0].body_to_world.position = np.array([10, 20, 30])
objects[0].body_to_world.set_rotation(np.array([0.1, 0.2, 0.3]))
objects[0].velocity = np.array([2, 3, 4])
objects[0].dimensions = np.array([1, 1, 1])
objects[0].properties["num_points"] = '[100]'
objects[0].properties["attributes"] = '["eats_icecream", "carries_bag"]'
objects[1].id = 2
objects[1].creation_ts = 100
objects[1].timestamp = 200
objects[1].class_id = 2
objects[1].class_confidence = 0.8
objects[1].object_to_body.position = np.array([3, 2, 1])
objects[1].object_to_body.set_rotation(np.array([1, 1, 1]))
objects[1].body_to_world.position = np.array([4, 5, 6])
objects[1].body_to_world.set_rotation(np.array([0.4, 0.5, 0.6]))
objects[1].velocity = np.array([4, 3, 2])
objects[1].dimensions = np.array([2, 2, 2])
objects[1].properties["num_points"] = '[50]'
objects[1].properties["attributes"] = '["parked_illegaly"]'
frame.objects["test_objects"] = objects
using namespace ouster::sdk;
std::vector<core::Object> objects(2);
objects[0].id = 1;
objects[0].creation_ts = 99;
objects[0].timestamp = 199;
objects[0].class_id = 1;
objects[0].class_confidence = 0.9;
objects[0].object_to_body.set_position(Eigen::Vector3d{1, 2, 3});
objects[0].object_to_body.set_rotation(Eigen::Vector3d{2, 2, 2});
objects[0].body_to_world.set_position(Eigen::Vector3d{10, 20, 30});
objects[0].body_to_world.set_rotation(Eigen::Vector3d{0.1, 0.2, 0.3});
objects[0].velocity = core::UnalignedVector3f{2, 3, 4};
objects[0].dimensions = core::UnalignedVector3f{1, 1, 1};
objects[0].properties["num_points"] = "[100]";
objects[0].properties["attributes"] = "\[\"eats_icecream\", \"carries_bag\"]";
objects[1].id = 2;
objects[1].creation_ts = 100;
objects[1].timestamp = 200;
objects[1].class_id = 2;
objects[1].class_confidence = 0.8;
objects[1].object_to_body.set_position(Eigen::Vector3d{3, 2, 1});
objects[1].object_to_body.set_rotation(Eigen::Vector3d{1, 1, 1});
objects[1].body_to_world.set_position(Eigen::Vector3d{4, 5, 6});
objects[1].body_to_world.set_rotation(Eigen::Vector3d{0.4, 0.5, 0.6});
objects[1].velocity = core::UnalignedVector3f{4, 3, 2};
objects[1].dimensions = core::UnalignedVector3f{2, 2, 2};
objects[1].properties["num_points"] = "\"[50]\"";
objects[1].properties["attributes"] = "[\"parked_illegaly\"]";
frame.objects()["test_objects"] = objects;
Using an approach like the above, FrameSet can also store Object instances. Since OSF supports saving
LidarFrame and FrameSet, reading and writing datasets with objects can be done with
OsfFrameSetSource and
Writer or
AsyncWriter.
Reading and writing ClassMap¶
In Ouster SDK 1.0, no DetectionEngine implementation produces Object s with classifications, yet. However,
ClassMap, which creates an association between Object::class_id and a string, can still be useful for Object
s imported from an external source, like an annotated lidar dataset.
ClassMaps are stored in a FrameSetSourceMetadataSet and associated with a frame source via metadata keys (for example, "class_maps").
Writing an OSF with ClassMaps is straightforward, since the
Writer and
AsyncWriter support saving
FrameSetSourceMetadataSet like so:
writer = osf.Writer(str(output_path), [sensor_info])
class_map1 = core.ClassMap({
1: 'dog',
2: 'cat'
})
class_map2 = core.ClassMap({
1: 'tree',
2: 'bush'
})
class_maps = core.ClassMapSet({
'four_legs': class_map1,
'zero_legs': class_map2
})
metadata = core.FrameSetSourceMetadataSet()
metadata['class_maps'] = class_maps
metadata['additional_info'] = "Test additional info"
writer.save(metadata)
osf::Writer writer(output_osf_filename, {sensor_info});
core::ClassMap class_map1;
class_map1.class_map.emplace(1,
"dog");
class_map1.class_map.emplace(2,
"cat");
core::ClassMap class_map2;
class_map2.class_map.emplace(1,
"tree");
class_map2.class_map.emplace(2,
"bush");
core::ClassMapSet class_maps;
class_maps.class_maps.emplace("four_legs",
class_map1);
class_maps.class_maps.emplace("zero_legs",
class_map2);
core::FrameSetSourceMetadataSet frame_set_source_metadata_set;
frame_set_source_metadata_set.entries.emplace("class_maps", class_maps);
std::string additional_info = "Test additional info";
frame_set_source_metadata_set.entries.emplace("additional_info", additional_info);
writer.save(frame_set_source_metadata_set);
Reading a ClassMap from a FrameSetSource is as simple as accessing it using its string key from the frame set source metadata.
from ouster.sdk import open_source
src = open_source(str(osf_path))
# Unlike C++, no .is<>) check is needed: metadata() returns a
# ClassMapSet directly (the bindings unwrap the stored type for you).
class_map_set = src.metadata('class_maps')
dog_class = class_map_set['four_legs'][1]
using namespace ouster::sdk;
auto src = open_source(osf_path);
// In C++, FrameSetSource metadata uses type erasure to allow storing
// different types of metadata So the type must be checked and the value
// casted to the appropriate type.
if (!src.metadata("class_maps").is<core::ClassMapSet>()) {
throw std::runtime_error("Unexpected metadata type!");
}
auto class_map_set = src.metadata("class_maps").as<core::ClassMapSet>();
auto dog_class = class_map_set.class_maps["four_legs"].class_map[1];