In order to run this example, a device supporting pose stream (T265) is required.
This sample demonstrates how to draw the trajectory of the device's movement based on pose data.
The application should open a window split into 4 viewports: top (upper left viewport), front (lower left viewport), side (lower right viewport) and 3D (upper right viewport). In each viewport, a 3D model of the camera and the corresponding 2D trajectory are rendered. In the 3D view, you should be able to interact with the camera using your mouse, for rotating, zooming and panning.
First, we include the Intel® RealSense™ Cross-Platform API.
#include <librealsense2/rs.hpp> // Include RealSense Cross Platform API
In this example we will also use the auxiliary library of example.hpp
:
#include "../example.hpp"
We define a struct tracked_point
to store a single point of the trajectory with it's confidence level.
struct tracked_point
{
rs2_vector point;
unsigned int confidence;
};
We'll use the class tracker
to keep track of the device's movement: calculate new transformation matrices, store trajectory points and draw them.
This class holds a vector of the trajectory's points and also the minimal and maximal coordinates, to allow zooming out according to the trajectory's size.
std::vector<tracked_point> trajectory;
rs2_vector max_coord;
rs2_vector min_coord;
The function calc_transform
calculates the transformation matrix in relation to the initial state, based on pose data from the device. It uses the rotation data in quaternions and translation data in meters to compute the resulting matrix. Note that we use coulmn-major representation as required by OpenGL. A rotation by 180 degrees in the y axis is applied to set the correct orientation.
void calc_transform(rs2_pose& pose_data, float mat[16])
{
auto q = pose_data.rotation;
auto t = pose_data.translation;
// Set the matrix as column-major for convenient work with OpenGL and rotate by 180 degress (by negating 1st and 3rd columns)
mat[0] = -(1 - 2 * q.y*q.y - 2 * q.z*q.z); mat[4] = 2 * q.x*q.y - 2 * q.z*q.w; mat[8] = -(2 * q.x*q.z + 2 * q.y*q.w); mat[12] = t.x;
mat[1] = -(2 * q.x*q.y + 2 * q.z*q.w); mat[5] = 1 - 2 * q.x*q.x - 2 * q.z*q.z; mat[9] = -(2 * q.y*q.z - 2 * q.x*q.w); mat[13] = t.y;
mat[2] = -(2 * q.x*q.z - 2 * q.y*q.w); mat[6] = 2 * q.y*q.z + 2 * q.x*q.w; mat[10] = -(1 - 2 * q.x*q.x - 2 * q.y*q.y); mat[14] = t.z;
mat[3] = 0.0f; mat[7] = 0.0f; mat[11] = 0.0f; mat[15] = 1.0f;
}
The function add_to trajectory
appends a new point to the trajectory vector. The first point is always added and set as the min and max coordinates.
void add_to_trajectory(tracked_point& p)
{
// If first element, add to trajectory and initialize minimum and maximum coordinates
if (trajectory.size() == 0)
{
trajectory.push_back(p);
max_coord = p.point;
min_coord = p.point;
}
If the point is not the first one, check if it's far enough from the previous point. If the two last points are too close (less than 1 mm apart), only the one with higher confidence stays in the trajectory. This way, points which don't add new information are discarded to save space. If a new point is inserted, maximum and minimum coordinates are updated.
else
{
// Check if new element is far enough - more than 1 mm (to keep trajectory vector as small as possible)
rs2_vector prev = trajectory.back().point;
rs2_vector curr = p.point;
if (sqrt(pow((curr.x - prev.x), 2) + pow((curr.y - prev.y), 2) + pow((curr.z - prev.z), 2)) < 0.001)
{
// If new point is too close to previous point and has higher confidence, replace the previous point with the new one
if (p.confidence > trajectory.back().confidence)
{
trajectory.back() = p;
update_min_max(p.point);
}
}
else
{
// If new point is far enough, add it to trajectory
trajectory.push_back(p);
update_min_max(p.point);
}
}
draw_trajectory
draws all points stored in the trajectory vector. Low confidence points are colored red, medium confidence - yellow, and high confidence is colored green. Grey points indicate failure in pose retrieval.
void draw_trajectory()
{
float3 colors[]{
{ 0.7f, 0.7f, 0.7f },
{ 1.0f, 0.0f, 0.0f },
{ 1.0f, 1.0f, 0.0f },
{ 0.0f, 1.0f, 0.0f },
};
glLineWidth(2.0f);
glBegin(GL_LINE_STRIP);
for (auto&& v : trajectory)
{
auto c = colors[v.confidence];
glColor3f(c.x, c.y, c.z);
glVertex3f(v.point.x, v.point.y, v.point.z);
}
glEnd();
glLineWidth(0.5f);
}
Class view
is a base class which provides functions for rendering the four views on screen. It holds a reference to tracker
and camera_renderer
objects, which allow rendering of the trajectory and the 3D camera model, respectively.
protected:
float width, height;
float aspect;
tracker& t;
private:
camera_renderer& renderer;
The function load_matrices
sets a viewport, using position (pos
) and size (app_width
, app_height
) passed as parameters. We use glScissor
to avoid rendering outside of the viewport window.
void load_matrices(float2 pos, float app_width, float app_height)
{
width = app_width;
height = app_height;
aspect = height / width;
// Set viewport to 1/4 of the app window and enable scissor test to avoid rendering outside the current viewport
glViewport(pos.x, pos.y, width / 2, height / 2);
glEnable(GL_SCISSOR_TEST);
glScissor(pos.x, pos.y, width / 2, height / 2);
We use glOrtho
to set the projection matrix. We define the viewport to cover 2 meters in width, the origin being in the middle of the viewport window. This will allow us to conveniently keep track of the viewport scale in meters.
// Setup orthogonal projection matrix
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glOrtho(-1.0, 1.0, -1.0 * aspect, 1.0 * aspect, -100.0, 100.0);
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
}
clean_matrices
returns the viewport to it's default configuration, by popping matrices pushed in load_matrices
and setting the viewport back to full size.
void clean_matrices()
{
// Pop LookAt matrix
glPopMatrix();
// Pop Projection matrix
glMatrixMode(GL_PROJECTION);
glPopMatrix();
// Set viewport back to full screen
glViewport(0, 0, width, height);
glDisable(GL_SCISSOR_TEST);
}
draw_cam_trajectory
renders both the trajectory and the camera model. The parameters angle
and axes
define the initial position of the camera and trajectory, to match the direction of the 2 axes shown in each viewport.
void draw_cam_trajectory(float angle, float3 axes, float r[16])
{
glPushMatrix();
// Set initial camera position (rotate by given angle)
glRotatef(angle, axes.x, axes.y, axes.z);
t.draw_trajectory();
Matrix r
is the trasnformation matrix calculated from the device. We apply this matrix before rendering the camera, so that the camera model moves according to the device's actual movement.
glMultMatrixf(r);
renderer.render_camera();
glPopMatrix();
}
Class view_2d
inherits from view
and is responsible for handling the 2D viewports. In addition to fields stored in view
, view_2d
also has members needed to zoom-out the view according to trajectory size: window_borders
stores the maximum coordinates of the viewport window; scale_factor
holds the current scale in relation to the initial state. lookat_eye
is used to set a different perspective for each viewport.
draw_view
renders the 2D view. It accepts as parameters the position of the viewport, it's size, the position of the scale bar - scale_pos
and the transformation matrix r
.
First, we call display_scale
which calculates the current scale of the viewport in meters and prints it. It also returns the width of the scale bar which matches the calculated scale.
void draw_view(float2 pos, float width, float height, float2 scale_pos, float r[16])
{
// Calculate and print scale in meters
float bar_width = display_scale(scale_factor, scale_pos.x, scale_pos.y);
We set min_coord
and max_coord
which we'll use to check if zooming-out is needed.
load_matrices
is called to prepare the viewport for rendering and the scale bar is rendered using bar_width
we previously obtained.
rs2_vector min_coord = t.get_min_coord();
rs2_vector max_coord = t.get_max_coord();
// Prepare viewport for rendering
load_matrices(pos, width, height);
glBegin(GL_LINES);
// Draw scale bar
glColor3f(1.0f, 1.0f, 1.0f);
glVertex3f(0.8, -0.9 * aspect, 0); glVertex3f(0.8 + bar_width, -0.9 * aspect, 0);
glEnd();
We define the perspective of the viewport using gluLookAt
and scale according to the current scale_factor
.
// Set a 2D view using OpenGL's LookAt matrix
gluLookAt(lookat_eye.x, lookat_eye.y, lookat_eye.z, 0, 0, 0, 0, 1, 0);
// Draw axes (only two are visible)
draw_axes();
// Scale viewport according to current scale factor
glScalef(scale_factor, scale_factor, scale_factor);
If the trajectory's new maximal or minimal coordinates are beyond the window borders, we zoom out by scaling the viewport, in order to keep the whole trajectory visible. scale_factor
and window_borders
are updated accordingly. Then, we render the camera and trajectory using draw_cam_trajectory
with 180 degrees rotation in the y axis.
// If trajectory reached one of the viewport's borders, zoom out and update scale factor
if (min_coord.*a < -window_borders.x || max_coord.*a > window_borders.x
|| min_coord.*b < -window_borders.y || max_coord.*b > window_borders.y)
{
glScalef(0.5, 0.5, 0.5);
scale_factor *= 0.5;
window_borders.x = window_borders.x * 2;
window_borders.y = window_borders.y * 2;
}
// Draw trajectory and camera
draw_cam_trajectory(180, { 0, 1, 0 }, r);
// Return the default configuration of OpenGL's matrices
clean_matrices();
Similarly, class view_3d
handles rendering of the 3D view. We use the function render_scene
to handle manipulations by the user. draw_cam_trajectory
is called with a parameter of 90 angles rotation in the x angle.
void draw_view(float2 pos, float app_width, float app_height, glfw_state app_state, float r[16])
{
// Prepare viewport for rendering
load_matrices(pos, app_width, app_height);
// Set the scene configuration and handle user's manipulations
render_scene(app_state);
// Draw trajectory and camera
draw_cam_trajectory(90, { 1, 0, 0 }, r);
// // Return the default configuration of OpenGL's matrices
clean_matrices();
}
The class split_screen_renderer
handles the 4 viewports. It stores 3 objects of view_2d
: top
, front
and side
, and one object of view_3d
: three_dim
.
draw_windows
calls each object's draw_view
function, and also handles some graphical functionalities: drawing the labels of the axes, as well as each viewport's title and the borders between viewports.
void draw_windows(float app_width, float app_height, glfw_state app_state, float r[16])
{
// Clear screen
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Upper left window: top perspective (x and z axes)
top.draw_view({ 0, app_height / 2 }, app_width, app_height, float2{ float(0.457) * app_width, float(0.49) * app_height }, r);
// Lower left window: front perspective (x and y axes)
front.draw_view({ 0, 0 }, app_width, app_height, float2{ float(0.457) * app_width, float(0.99) * app_height }, r);
// Lower right window: side perspective (y and z axes)
side.draw_view({ app_width / 2, 0 }, app_width, app_height, float2{ float(0.957) * app_width, float(0.99) * app_height }, r);
// Upper right window: 3D view
three_dim.draw_view({ app_width / 2, app_height / 2 }, app_width, app_height, app_state, r);
In the main function, we first initialize the window
as well as the camera_renderer
, tracker
and split_screen_renderer
objects.
int main(int argc, char * argv[]) try
{
// Initialize window for rendering
window app(1280, 720, "RealSense Trajectory Example");
// Construct an object to manage view state
glfw_state app_state(0.0, 0.0);
// Register callbacks to allow manipulation of the view state
register_glfw_callbacks(app, app_state);
// Create objects for rendering the camera, the trajectory and the split screen
camera_renderer cam_renderer;
tracker tracker;
split_screen_renderer screen_renderer(app.width(), app.height(), tracker, cam_renderer);
Then, we initialize rs2::pipeline
. We configure it with RS2_STREAM_POSE
since we'll only use pose frames, and start the pipeline.
// Declare RealSense pipeline, encapsulating the actual device and sensors
rs2::pipeline pipe;
// Create a configuration for configuring the pipeline with a non default profile
rs2::config cfg;
// Add pose stream
cfg.enable_stream(RS2_STREAM_POSE, RS2_FORMAT_6DOF);
// Start pipeline with chosen configuration
pipe.start(cfg);
In the main loop, we obtain the next pose frame and get it's pose_data
.
while (app)
{
// Wait for the next set of frames from the camera
auto frames = pipe.wait_for_frames();
// Get a frame from the pose stream
auto f = frames.first_or_default(RS2_STREAM_POSE);
// Cast the frame to pose_frame and get its data
auto pose_data = f.as<rs2::pose_frame>().get_pose_data();
Using the pose_data
, we can calculate the transformation matrix. We use a one dimensional array of length 16 to store the matrix, for convenient work with OpenGL.
float r[16];
// Calculate current transformation matrix
tracker.calc_transform(pose_data, r);
The next point of the trajectory is obtained from the computed transformation matrix. Along with it's confidence level, we try adding the new point to the trajectory vector using add_to_trajectory
function.
// From the matrix we found, get the new location point
rs2_vector tr{ r[12], r[13], r[14] };
// Create a new point to be added to the trajectory
tracked_point p{ tr , pose_data.tracker_confidence };
// Register the new point
tracker.add_to_trajectory(p);
Finally, we draw the trajectory of the camera movement so far and render a 3D model of the camera according to it's current pose. The rendering is done from different perspectives in each of the viewports using draw_windows
as described above.
// Draw the trajectory from different perspectives
screen_renderer.draw_windows(app.width(), app.height(), app_state, r);