Skip to main content
Open In Colab The EKF SLAM section used the camera only to measure landmarks, and the robot’s motion came from its wheels. A visual SLAM system estimates the camera’s motion from the images themselves. It tracks points from frame to frame, recovers how the camera moved between frames, triangulates the points into a map, and fixes the accumulated drift when the camera returns to a place it has seen before. StellaVSLAM, which you run in the Visual SLAM assignment, is organized this way. This section builds each of those steps on a synthetic scene. Every pixel, pose and point has a known true value, so each source of error can be measured on its own. Real images add the work of detecting and matching features, which the assignment covers.

Features in visual SLAM and VIO

Visual SLAM and visual-inertial odometry (VIO) work with features too, but each part of the range, bearing and signature triple of the landmark model changes. A monocular camera does not measure range. A keypoint at pixel (u,v)(u, v) fixes only the ray from the camera through the landmark. Multiplying by the inverse intrinsics, K−1(u,v,1)⊤K^{-1}(u, v, 1)^\top, turns the pixel into a direction in 3D, a bearing in both azimuth and elevation. The landmarks are 3D points pjp_j, and the measurement model is the pinhole projection of the point into a camera with rotation RR and position tt: zti=[utivti]=π ⁣(KR⊤(pj−t))+ε,π(a,b,c)=(ac,bc).z^i_t = \begin{bmatrix} u^i_t \\ v^i_t \end{bmatrix} = \pi\!\left(K R^\top (p_j - t)\right) + \varepsilon, \qquad \pi(a, b, c) = \left(\frac{a}{c}, \frac{b}{c}\right). The division by depth in π\pi is why one view gives no range. The residual zti−z^tiz^i_t - \hat z^i_t is the reprojection error, in pixels. A stereo pair measures the disparity between its two images, which gives depth, and an RGB-D camera measures depth at each pixel. With a single camera, a new landmark is triangulated from two or more views taken far enough apart, and the map is known only up to scale. VIO adds an IMU, whose accelerometer fixes the metric scale and the direction of gravity. The signature becomes a descriptor computed from the image patch around the keypoint, such as a 256-bit ORB descriptor or a 128-dimensional SIFT descriptor. A descriptor resembles its landmark but does not name it. Two corners can look alike, and one corner looks different from another viewpoint. The correspondences are therefore unknown. They come from matching descriptors, or from tracking patches from frame to frame, and the wrong matches are removed geometrically, for example with RANSAC on the essential matrix. The rest of this section builds this pipeline. A frame also has hundreds of keypoints rather than a few landmarks, which changes how the estimators are built. Filter-based VIO such as the MSCKF keeps a window of past camera poses in its state but not the points. Each point is triangulated when its track ends and then constrains only the poses. Optimization-based systems such as ORB-SLAM adjust the keyframe poses and the points together to minimize the reprojection error, which is bundle adjustment. VINS-Mono does the same over a sliding window and adds IMU terms between keyframes.

The scene

The camera moves around a circle of radius 1.5 m in a 6 m by 6 m room, 0.5 m above the floor and looking along its direction of travel. It records 60 keyframes on the way, about 16 cm apart, and a 61st that repeats the pose of the first, so the loop closes. The room holds 1500 points: half on the walls and half on furniture spread through the room, kept at least 0.5 m away from the path. The camera has the nominal intrinsics of the real D435i color camera from the course’s camera configuration, fx=fy=617f_x = f_y = 617 px and principal point (321, 240) in a 640 by 480 image, and the same optical frame as the VIO section: zz forward, xx right, yy down. Each keyframe records the pixels of the points in its view with one pixel of noise. Matching is by point index, the stand-in for a feature descriptor. To imitate wrong descriptor matches, 5% of each keyframe’s pixels are replaced by a random pixel, so about one match in ten between two keyframes is wrong.
Output from cell 4

Motion between two views

Two views of the same points satisfy the epipolar constraint from the stereo section. With calibrated cameras it involves the essential matrix, x^b⊤E x^a=0,E=[t]×R,\hat x_b^\top E\, \hat x_a = 0, \qquad E = [t]_\times R, where x^a=K−1(ua,va,1)⊤\hat x_a = K^{-1}(u_a, v_a, 1)^\top are normalized image coordinates and xb=Rxa+tx_b = R x_a + t moves points from camera aa to camera bb. Five matches determine EE. A wrong match ruins any estimate it takes part in, so the estimate is wrapped in RANSAC, as in the homographies section. RANSAC repeatedly fits EE to five random matches, counts the matches within 3 pixels of their epipolar line, and keeps the fit with the most. EE yields four candidate motions, from two rotations and two signs of tt. Only one of them puts the triangulated points in front of both cameras, and recoverPose picks that one. The translation comes out with unit length: the images do not contain the length of the baseline, because the same pictures result from a scene twice as large viewed by a camera that moved twice as far. With RR and tt fixed, each inlier match is triangulated into a 3D point in camera aa‘s frame.
Output from cell 7 Each line joins a point’s pixel in keyframe 0 to its pixel in keyframe 1. Most inlier lines run sideways by a similar amount: between the two keyframes the camera turns by 6 degrees, which shifts the whole image, and the forward motion adds a smaller spread away from the image center. The long lines are the wrong matches. RANSAC rejects most of them, while a wrong match that happens to lie near its epipolar line passes. The estimated rotation is half a degree off. Over a 16 cm baseline with one pixel of noise the translation direction is the least precise part of the estimate, about 5 degrees off here.

Visual odometry

Chaining the two-view estimates gives visual odometry. The first pair’s baseline defines the unit of length. Every later pair also returns a unit translation, so its length is rescaled to keep the map consistent: points seen by the pair before and by the current pair must come out at the same distance from the shared camera, and the median ratio of the two distances sets the scale. The function ate measures the result. It finds the similarity, a rotation, a translation and a scale, that best aligns the estimated camera positions with the true ones, and reports the remaining root-mean-square distance, the absolute trajectory error.
RANSAC, followed by the check that each point lies in front of both cameras, keeps 84% of the matches on average, while 90% of the matches are correct. The chained trajectory comes out 7.5 times larger than the true one. Most of that factor is the choice of unit: the first baseline is 0.157 m, which alone would give a factor of 6.4. The rest is scale drift accumulated along the loop. A single camera cannot measure that factor. The D435i’s depth or its IMU can, which is why turtlebot-maze can run StellaVSLAM in RGB-D mode with the camera’s depth images. The scale also drifts. Each rescaling uses a few hundred noisy depths, and every later step inherits its error. After the best alignment the trajectory error is 0.129 m, and the end of the loop misses its start by 0.492 m.

Loop closure

At the last keyframe the camera is back at its starting pose. A real system recognizes this from the images, typically by comparing a bag-of-words summary of each keyframe’s descriptors. Here the shared point indices reveal it directly: most points in view were already mapped near the start. The function below relocalizes the last keyframe against that early map. Given known 3D points and their pixels, the camera pose follows from Perspective-n-Point (PnP), again wrapped in RANSAC. The same points, triangulated by the last pair of the chain, give the scale drift. Their distances from the camera in the chain’s units, compared with their distances in the units of the start map, show how far the unit of length has drifted around the loop.
Only 37 of the 107 points pass PnP’s 2-pixel test. The start map was triangulated from pairs 16 cm apart, so many of its points, especially the distant ones, have poor depth. A real system refines the map with bundle adjustment: it adjusts the camera poses and the 3D points together to minimize the total reprojection error over every keyframe that sees each point. Chapter 44 of Torralba, Isola and Freeman’s Foundations of Computer Vision develops it.

Pose graph optimization

The loop measurement disagrees with the chain, and the error has to be spread over all the keyframes instead of being dumped on the last one. A pose graph does this. Each keyframe is a node with a pose and a scale, a similarity transform in Sim(3). Each consecutive pair of keyframes is joined by an edge that holds their relative motion from visual odometry, at unchanged scale. The loop closure adds one more edge, from keyframe 0 to the last keyframe, carrying the PnP pose and the measured scale drift. The optimizer moves the nodes to minimize the total disagreement with all the edges, keeping keyframe 0 fixed. Each odometry edge gives way a little, and the loop edge is satisfied. Sim(3) rather than the rigid motions of SE(3) is the choice for a single camera: the scale drift is a real error in the chain, and only a node with its own scale can absorb it.
Output from cell 14 The left plot is the chained trajectory in the coordinates of the first camera, measured in units of the first baseline. It has the right shape at the wrong size, and it does not close. The right plot aligns each estimate to the truth. The pose graph pulls the loop shut, from a 0.492 m gap to 0.029 m, and the trajectory error drops from 0.129 m to 0.039 m.

The same steps in StellaVSLAM

StellaVSLAM, the maintained fork of OpenVSLAM, splits the work into three modules (Sumikura et al., 2019): In the course’s turtlebot-maze repository, slam/slam_bridge.py takes the simulated D435i’s color images, and in RGB-D mode its depth images, and streams them to slam/run_slam.cc, which feeds them to StellaVSLAM and returns the pose of every frame. The assignment’s Task 1 builds the front end of this section on real KITTI images, where finding and matching the features is the hard part. Task 2 runs StellaVSLAM on video of your own room. Key references: (Sumikura et al., 2019; Corke, 2023)

References

  • Corke, P. (2023). Robotics, vision and control: Fundamental algorithms in python.
  • Sumikura, S., Shibuya, M., Sakurada, K. (2019). OpenVSLAM: A versatile visual SLAM framework. arXiv [cs.CV].

Found an error on this page? Report an issue.