The following is an overview of the functionality provided by the Cepton SDK. We will start at a high-level overview of the architecture, then dive deeper into some of the specific functions provided.
The SDK is based around the ideas of parsers and callbacks.
Let's start by taking a closer look at the different data being output from the sensors and SDK.
struct CeptonPoint {
int16_t x;
uint16_t y;
int16_t z;
uint8_t reflectivity;
uint8_t relative_timestamp;
uint8_t channel_id;
uint8_t flags;
};
Fields description:
x: int16_t
: the x position component, that is, the horizontal axes. Negative x are to the left, positive x are to the right.
y: uint16_t
: the y position component, that is, the depth axis. Negative y points out of the screen, positive y points into the screen.
z: int16_t
: the z position component, that is, the vertical axis. Negative z points down, positive z points up.
reflectivity: uint8_t
: the reflectivity. For values 0-127, this is measured linearly, i.e. point.reflectivity == 20 is 20%, but beyond that the correlation scales exponentially, up to point.reflectivity == 255 is 5000%. If your processor has a floating point unit, the simplest thing is to copy this lookup table over, and use the point.reflectivity
value as the index. The values in this lookup table range from 0 to 50. 50 = reflectance of 5000%.
channel_id: uint8_t
: identify the lazer that fired this point
flags: uint8_t
: The bits can be masked with the following enum to get each flag. Some of these flags may be sensor dependent.
// Reflectivity look up table
static const float reflectivity_LUT[256] = {
0.000f, 0.010f, 0.020f, 0.030f, 0.040f, 0.050f, 0.060f, 0.070f,
0.080f, 0.090f, 0.100f, 0.110f, 0.120f, 0.130f, 0.140f, 0.150f,
0.160f, 0.170f, 0.180f, 0.190f, 0.200f, 0.210f, 0.220f, 0.230f,
0.240f, 0.250f, 0.260f, 0.270f, 0.280f, 0.290f, 0.300f, 0.310f,
0.320f, 0.330f, 0.340f, 0.350f, 0.360f, 0.370f, 0.380f, 0.390f,
0.400f, 0.410f, 0.420f, 0.430f, 0.440f, 0.450f, 0.460f, 0.470f,
0.480f, 0.490f, 0.500f, 0.510f, 0.520f, 0.530f, 0.540f, 0.550f,
0.560f, 0.570f, 0.580f, 0.590f, 0.600f, 0.610f, 0.620f, 0.630f,
0.640f, 0.650f, 0.660f, 0.670f, 0.680f, 0.690f, 0.700f, 0.710f,
0.720f, 0.730f, 0.740f, 0.750f, 0.760f, 0.770f, 0.780f, 0.790f,
0.800f, 0.810f, 0.820f, 0.830f, 0.840f, 0.850f, 0.860f, 0.870f,
0.880f, 0.890f, 0.900f, 0.910f, 0.920f, 0.930f, 0.940f, 0.950f,
0.960f, 0.970f, 0.980f, 0.990f, 1.000f, 1.010f, 1.020f, 1.030f,
1.040f, 1.050f, 1.060f, 1.070f, 1.080f, 1.090f, 1.100f, 1.110f,
1.120f, 1.130f, 1.140f, 1.150f, 1.160f, 1.170f, 1.180f, 1.190f,
1.200f, 1.210f, 1.220f, 1.230f, 1.240f, 1.250f, 1.260f, 1.270f,
1.307f, 1.345f, 1.384f, 1.424f, 1.466f, 1.509f, 1.553f, 1.598f,
1.644f, 1.692f, 1.741f, 1.792f, 1.844f, 1.898f, 1.953f, 2.010f,
2.069f, 2.129f, 2.191f, 2.254f, 2.320f, 2.388f, 2.457f, 2.529f,
2.602f, 2.678f, 2.756f, 2.836f, 2.919f, 3.004f, 3.091f, 3.181f,
3.274f, 3.369f, 3.467f, 3.568f, 3.672f, 3.779f, 3.889f, 4.002f,
4.119f, 4.239f, 4.362f, 4.489f, 4.620f, 4.754f, 4.892f, 5.035f,
5.181f, 5.332f, 5.488f, 5.647f, 5.812f, 5.981f, 6.155f, 6.334f,
6.519f, 6.708f, 6.904f, 7.105f, 7.311f, 7.524f, 7.743f, 7.969f,
8.201f, 8.439f, 8.685f, 8.938f, 9.198f, 9.466f, 9.741f, 10.025f,
10.317f, 10.617f, 10.926f, 11.244f, 11.572f, 11.909f, 12.255f, 12.612f,
12.979f, 13.357f, 13.746f, 14.146f, 14.558f, 14.982f, 15.418f, 15.866f,
16.328f, 16.804f, 17.293f, 17.796f, 18.314f, 18.848f, 19.396f, 19.961f,
20.542f, 21.140f, 21.755f, 22.389f, 23.040f, 23.711f, 24.401f, 25.112f,
25.843f, 26.595f, 27.369f, 28.166f, 28.986f, 29.830f, 30.698f, 31.592f,
32.511f, 33.458f, 34.432f, 35.434f, 36.466f, 37.527f, 38.620f, 39.744f,
40.901f, 42.092f, 43.317f, 44.578f, 45.876f, 47.211f, 48.586f, 50.000f,
};
CeptonPoint point = ...
float reflectivity = reflectivity_LUT[point.reflectivity];
enum {
CEPTON_POINT_SATURATED = 1 << 0,
CEPTON_POINT_BLOOMING = 1 << 1,
CEPTON_POINT_FRAME_PARITY = 1 << 2,
CEPTON_POINT_FRAME_BOUNDARY = 1 << 3,
CEPTON_POINT_SECOND_RETURN = 1 << 4,
CEPTON_POINT_NO_RETURN = 1 << 5,
CEPTON_POINT_NOISE = 1 << 6,
CEPTON_POINT_BLOCKED = 1 << 7,
};
struct CeptonPointEx {
int32_t x; // Unit is 1/65536 m or ~0.015mm
int32_t y;
int32_t z;
uint16_t reflectivity; // Unit is 1%, no lookup table
uint16_t relative_timestamp; // Unit is 1 us
uint16_t flags;
uint16_t channel_id;
};
In general, you don't need worry about using parsers for Cepton lidars. The SDK has implemented parsers for all our data types.
APIs to be aware of:
CeptonRegisterParser
CeptonUnregisterParser
Callbacks are the part of the API that you will be using.
APIs to be aware of:
CeptonListenPoints()
CeptonUnlistenPoints()
CeptonListenPointsEx()
CeptonUnlistenPointsEx()
CeptonListenFrames()
CeptonUnlistenFrames()
CeptonListenFramesEx()
CeptonUnlistenFramesEx()
Though there are a lot of functions here, don't worry. They are broken into two main categories: Listen and ListenEx functions. For VistaP, VistaX90, and Nova, you can use the non-ex functions. The EX callbacks are for X120-Ultra and other newer lidars that require extended data.
Let's take a look at the simplest function, CeptonListenPoints
.
int CeptonListenPoints(CeptonPointsCallback callback, void *user_data);
Points
callbacks are invoked whenever a new UDP data packet comes in, which will contain ~144 points.
There are two parameters: callback
and user_data
. Let's first take a look at the callback
, and break down each of the parameters.
typedef void (*CeptonPointsCallback)(CeptonSensorHandle handle,
int64_t start_timestamp, size_t n_points,
size_t stride, const uint8_t *points,
void *user_data);
handle: CeptonSensorHandle
: this is a handle that can be used to identify data coming from each sensor. In general this will be the encoding of the sensor's IP address. For example, 192.168.32.32 => 0xc0a82020 => 3232243744
.start_timestamp: int64
: this is the timestamp of the data, measured in microseconds. If the sensor is PTP-synced, this will the PTP timestamp. If the sensor is not PTP-synced, it will be the sensor's power-up timestamp; that is, how long the sensor has been powered up.n_points: size_t
: the number of points in this datastride: size_t
: for the non-ex callbacks, all data is strided. This is for future compatibility in case the size of CeptonPoint
changes, but for now this will always be 10 bytes. This is used to index into points
points: const uint8_t*
: you can see here that this data is a pointer to some generic bytes, there is no type assigned. To get the i
'th point, you can do the following:CeptonPoint point = *(CeptonPoint const*)(points + i * stride);
user_data: void*
: this will be the pointer that was passed in as the user_data
parameter to CeptonListenPoints
. Depending on your use case, this can just be passed as nullptr
if you're only modifying global state. My typical use for this is to pass object pointers, if I have a callback that I want to access an instance of some object.void callback(CeptonSensorHandle handle,
int64_t start_timestamp, size_t n_points,
size_t stride, const uint8_t *points,
void *user_data);
class MyObject {
public:
MyObject() {}
size_t counter = 0;
void start() {
...
CeptonListenPoints(callback, this);
...
}
}
void callback(CeptonSensorHandle handle,
int64_t start_timestamp, size_t n_points,
size_t stride, const uint8_t *points,
void *user_data)
{
// Increment the object's counter
reinterpret_cast<MyObject*>(user_data)->counter += 1;
}
int main()
{
// ... Initialize the SDK ...
MyObject mobj;
mobj.start();
// See that the counter is being incremented
while (true) {
cout << mobj.counter << endl;
this_thread::sleep_for(milliseconds(100));
}
}
APIs to be aware of:
CeptonListenFrames
CeptonUnlistenFrames
CeptonListenFramesEx
CeptonUnlistenFramesEx
Getting packets of 144 points is great, but that is no where near the size of a full frame. Most useful processing will be done on a completed data, containing tens of thousands of points. Let's look at a couple ways to get these completed frames.
Each lidar has its own notion of a "natural frame". This is determined by the firmware and is exposed by the parity bit flag in each point.
To check the parity for a given point, you can do:
CeptonPoint point;
bool parity = (point.flag & CEPTON_POINT_FRAME_PARITY) == 0;
You don't need to worry about this because it's taken care of by the SDK frame aggregators, but the idea is that each time the parity changes, the frame is completed and published. So if we have a stream of incoming points, the logic looks like this:
0 0 0 0 0 1 <publish frame with parity 0> 1 1 1 1 0 <publish frame with parity 1>
Again, you don't need to worry about this.
The callback type is the same as the point callback we set up above. To subscribe to these natural frames, the code looks like this:
void callback(CeptonSensorHandle handle,
int64_t start_timestamp, size_t n_points,
size_t stride, const uint8_t *points,
void *user_data)
{
cout << "Got " << n_points << " points" << endl;
}
int main() {
// ... other initialization code
CeptonListenFrames(CEPTON_AGGREGATION_MODE_NATURAL, callback, nullptr);
}
The second option, is to used timed frames. Generally this would not be used for modern sensors, but it is an option in case you want the SDK to take care of accumulating longer frames, rather than doing so yourself.
Rather than using CEPTON_AGGREGATION_MODE_NATURAL
, we instead pass in an integer for the frame duration we want, measured in microseconds. So if we wanted to accumulate 0.2-second frames, the API call would be:
int main() {
// .. other initialization code
CeptonListenFrames(200000, callback, nullptr);
}
Note that the duration must be at least 1000 microseconds, anything less will return an SDK error.
// basic.c
#include <stdio.h>
#include <stdlib.h>
#include "cepton_sdk2.h"
void check_api_error(int err, char const *api) {
if (err != CEPTON_SUCCESS) {
printf("API Error for %s: %s\n", api, CeptonGetErrorCodeName(err));
exit(1);
}
}
int n_frames = 0;
void frameCallback(CeptonSensorHandle handle, int64_t start_timestamp,
size_t n_points, size_t stride, const uint8_t *points,
void *user_data) {
printf("Got %d frames: %d points\n", ++n_frames, (int)n_points);
}
int main() {
int ret;
// Initialize
ret = CeptonInitialize(CEPTON_API_VERSION, sensorErrorCallback);
check_api_error(ret, "CeptonInitialize");
ret = CeptonEnableLegacyTranslation();
check_api_error(ret, "EnableLegacyTranslation");
// Start networking listener thread
ret = CeptonStartNetworking();
check_api_error(ret, "CeptonStartNetworking");
// Listen for frames
ret = CeptonListenFrames(CEPTON_AGGREGATION_MODE_NATURAL, frameCallback, 0);
check_api_error(ret, "CeptonListenFrames");
// Sleep
while (n_frames < 10)
;
// Deinitialize
ret = CeptonDeinitialize();
check_api_error(ret, "CeptonDeinitialize");
return 0;
}
A couple notes on this sample:
CeptonStartNetworking()
. This starts an internal socket listener to receive the UDP data. If running with a live sensor, this function must be called after CeptonInitialize()
in order to receive any data.CeptonInitialize
, CEPTON_API_VERSION
. This variable is defined in cepton_sdk2.h
, and is used to ensure consistency between the included header file and the SDK binary. If the two do not match, an error is returned.CeptonEnableLegacyTranslation()
. Legacy sensors (VistaP, SoraP) use a different library which must loaded via this API. The legacy SDK binary must be located in the same directory as the SDK2 binaryFor Vista X90 and Nova sensors, CeptonPoint
is the data type naturally output by the sensor. For the X120 Ultra, 16 bits is no longer enough to encapsulate the max range measurement, so we introduce CeptonPointEx
, which has a couple nice additional features:
struct CeptonPointEx {
int32_t x; // Unit is 1/65536 m or ~0.015mm
int32_t y;
int32_t z;
uint16_t reflectivity; // Unit is 1%, no lookup table
uint16_t relative_timestamp; // Unit is 1 us
uint16_t flags;
uint16_t channel_id;
};
In any of the above code where CeptonListenFrames
, or CeptonListenPoints
was used, you can equivalently use CeptonListenFramesEx
and CeptonListenPointsEx
. This is compatible with any sensor that outputs the CeptonPoint
data above - the SDK will convert.
Let's revisit the above example, this time using EX points
// basic.c
#include <stdio.h>
#include <stdlib.h>
#include "cepton_sdk2.h"
void check_api_error(int err, char const *api) {
if (err != CEPTON_SUCCESS) {
printf("API Error for %s: %s\n", api, CeptonGetErrorCodeName(err));
exit(1);
}
}
int n_frames = 0;
void frameCallbackEx(CeptonSensorHandle handle, int64_t start_timestamp,
size_t n_points, const CeptonPointEx* points,
void *user_data) {
printf("Got %d frames: %d points\n", ++n_frames, (int)n_points);
// This is how we get the position in meters
float x_meters = points[0] / 65536.0;
float y_meters = points[1] / 65536.0;
}
int main() {
int ret;
// Initialize
ret = CeptonInitialize(CEPTON_API_VERSION, sensorErrorCallback);
check_api_error(ret, "CeptonInitialize");
ret = CeptonEnableLegacyTranslation();
check_api_error(ret, "EnableLegacyTranslation");
// Start networking listener thread
ret = CeptonStartNetworking();
check_api_error(ret, "CeptonStartNetworking");
// Listen for frames
ret = CeptonListenFramesEx(CEPTON_AGGREGATION_MODE_NATURAL, frameCallbackEx, 0);
check_api_error(ret, "CeptonListenFrames");
// Sleep
while (n_frames < 10)
;
// Deinitialize
ret = CeptonDeinitialize();
check_api_error(ret, "CeptonDeinitialize");
return 0;
}
A few things to note:
CeptonListenFrames/Points
, or CeptonListenFrameEx/PointsEx
, but not both.