Skip to the content
FixTheRobot Explore Robotics / UAV / Module 6 / M6-T10

UAV Systems Engineering · Module 6

M6-T10: External vision and odometry integration

Give PX4 a position it can trust from somewhere other than GNSS: get the frames, the timestamps and the covariance right, then set the parameters.

  • Lesson 10 of 16
  • UAV Software and Flight-Controller Integration
  • Implementation-specific
  • About 6 minutes to read

This is the integration that turns a research-grade VIO or LIO into a flying system: the autopilot’s own estimator accepts the companion computer’s odometry as a measurement. The interface is small, and nearly every failure comes from the same three details.

Learning objectives

After this lesson, you should be able to:

  • Explain what EKF2 expects in a VehicleOdometry message and where it is published.
  • Convert an ENU/FLU odometry message into the NED/FRD message PX4 expects.
  • Set the timestamp, variances and quality fields correctly.
  • Name the EKF2 parameters that control external-vision fusion and verify fusion in the logs.

Conceptual explanation

Implementation-specific Parameter names and defaults below are those of recent PX4 releases (v1.14 and later) as documented by PX4. They change between versions: read the “external position estimation” and EKF2 pages for your version before flying.

PX4’s EKF2 can fuse position, velocity and yaw from an external vision or odometry source. Over uXRCE-DDS the companion computer publishes a px4_msgs/VehicleOdometry message on /fmu/in/vehicle_visual_odometry (over MAVLink the equivalent messages are ODOMETRY and VISION_POSITION_ESTIMATE). EKF2 then treats it like any other measurement: it gates it by innovation, weights it by the supplied variances (or by fixed parameters), and uses it to correct the IMU-propagated state.

External odometry into EKF2A visual-inertial odometry node in ROS 2 ENU/FLU frames publishes an odometry message with covariance; a conversion node changes it to NED/FRD, stamps it with the sample time, and publishes VehicleOdometry on the visual odometry input topic of PX4 where EKF2 fuses it according to its parameters.VIO / LIOENU / FLUnav_msgs/Odometry Bridge nodeENU→NED, FLU→FRDtimestamp_sample, variances /fmu/in/vehicle_visual_odometrypx4_msgs/VehicleOdometry EKF2fuses perparameters wrong frame, stale timestamp or dishonest covariance: three silent ways to fail
A bridge node sits between the ROS 2 world and PX4: it converts frames, sets timestamps and passes covariance.
Animation of a delay scan for external vision. The vision position measurement is really 100 milliseconds old. As the assumed delay sweeps from 0 to 300 milliseconds, the plot of mean normalised innovation against assumed delay is drawn, falling to a minimum at the true delay of 100 milliseconds; beside it the measurement trace is shown shifted by the assumed delay against the estimate, and the two curves line up exactly at the minimum.
An external-vision delay parameter that is wrong inflates the innovations in proportion to speed, so the delay can be measured by scanning the assumed value and minimising the normalised innovation. Computed on a moving vehicle (peak speed 8 m/s) with 0.2 m measurement noise and a 0.1 m estimate accuracy. (Looping, about 6 seconds; drawn and computed for this course with illustrative values.)

Engineering intuition

There are three things EKF2 cannot know and you must tell it. Frame: is this NED/FRD or an arbitrary frame? A wrong frame yields a mirrored or rotated trajectory. Time: when was the measurement taken? Vision pipelines have 30–100 ms of latency; if the message is stamped with “now” instead of the exposure time, EKF2 corrects the wrong state and the error looks like random jitter. Trust: how uncertain is it? A VIO that reports a tiny, constant variance after tracking is lost will drag the estimate off with high confidence.

Mathematical formulation

Position and velocity convert as in lesson M1-T4. For the orientation quaternion, the rotation from FLU to ENU must become the rotation from FRD to NED. With qENU←FLU and the fixed frame rotations QNED←ENU and QFRD←FLU:

Orientation conversion
qNED←FRD=QNED←ENU⊗qENU←FLU⊗QFRD←FLU−1q_{\mathrm{NED}\leftarrow\mathrm{FRD}} = Q_{\mathrm{NED}\leftarrow\mathrm{ENU}} \otimes q_{\mathrm{ENU}\leftarrow\mathrm{FLU}} \otimes Q_{\mathrm{FRD}\leftarrow\mathrm{FLU}}^{-1}

Latency compensation: the measurement refers to the state at time tst_{s} (exposure), but arrives at ta=ts+δt_{a}=t_{s}+\delta . EKF2 keeps a short history of its states; given the sample time (timestamp_sample) or the delay parameter, it fuses the measurement against the state at tst_{s} and corrects forward. Without the true sample time, the estimator’s effective measurement error includes v⋅δv\cdot \delta : at 5 m/s with 80 ms of unmodelled delay that is 0.4 m.

Cost of a wrong timestamp
δp≈v δ\delta p \approx v\,\delta
Position error from an unmodelled delay δ.

Parameters (check your PX4 version)

EKF2 external vision parameters (names as of recent PX4 releases; verify for your version)
ParameterWhat it does
EKF2_EV_CTRLBitmask enabling which external-vision quantities are fused: horizontal position, vertical position, velocity, yaw.
EKF2_HGT_REFSelects the height source (barometer, GNSS, range sensor or vision).
EKF2_GPS_CTRLBitmask for GNSS fusion; set to 0 to disable GNSS fusion for a GNSS-denied configuration (older releases used EKF2_AID_MASK).
EKF2_EV_DELAYDelay of the vision measurement relative to the IMU if the sample time is not provided.
EKF2_EV_NOISE_MDChoose between the variances in the message and fixed noise parameters (EKF2_EVP_NOISE, EKF2_EVV_NOISE, EKF2_EVA_NOISE).

Checking the orientation conversion

The quaternion conversion is the easiest part to get wrong and the easiest to test without a vehicle. This runs as shown: a level vehicle facing east in ENU must become a 90∘90^{\circ} yaw in NED/FRD, and one facing north must become zero yaw.

import numpy as np

def qmul(a, b):
    w1, x1, y1, z1 = a; w2, x2, y2, z2 = b
    return np.array([w1*w2 - x1*x2 - y1*y2 - z1*z2, w1*x2 + x1*w2 + y1*z2 - z1*y2,
                     w1*y2 - x1*z2 + y1*w2 + z1*x2, w1*z2 + x1*y2 - y1*x2 + z1*w2])

S = np.sqrt(0.5)
Q_NED_ENU = np.array([0.0, S, S, 0.0]); Q_FRD_FLU = np.array([0.0, 1.0, 0.0, 0.0])
def enu_flu_to_ned_frd(q): return qmul(qmul(Q_NED_ENU, q), Q_FRD_FLU)

def yaw_of(q):                                    # yaw (rotation about the down axis) of a level quaternion [w, x, y, z]
    return np.degrees(np.arctan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] ** 2 + q[3] ** 2)))

east = np.array([1.0, 0, 0, 0])                                              # ENU yaw 0: facing east
north = np.array([np.cos(np.pi / 4), 0, 0, np.sin(np.pi / 4)])               # ENU yaw +90 deg (ccw from east): facing north
print("facing east -> NED yaw", round(yaw_of(enu_flu_to_ned_frd(east)), 3), "deg; facing north ->", round(yaw_of(enu_flu_to_ned_frd(north)), 3), "deg")
assert abs(abs(yaw_of(enu_flu_to_ned_frd(east))) - 90) < 1e-9 and abs(yaw_of(enu_flu_to_ned_frd(north))) < 1e-9
assert abs(np.linalg.norm(enu_flu_to_ned_frd(east)) - 1) < 1e-12
A frame-conversion unit test that needs no ROS, no PX4 and no vehicle.

Architecture and data flow

VIO produces a pose with covariance at camera rate → the bridge converts frames and fills timestamp (publication time, microseconds) and timestamp_sample (acquisition time), pose_frame and velocity_frame, position, q, velocity, the variance arrays and quality → /fmu/in/vehicle_visual_odometry → EKF2. Unknown quantities should be set to NaN, not zero: zero means “exactly zero”. Verify in the flight log that vision fusion is active and that innovations are small (lesson M6-T13).

Real-world UAV applications

  • GNSS-denied navigation indoors with VIO or LiDAR-inertial odometry.
  • Motion-capture position input for lab flights.
  • Position hold using external odometry in tunnels and warehouses.
  • Simulation of the whole chain in SITL with a simulated odometry source before flying.

Implementation example

A minimal bridge node (syntax-checked here, not executed by the tests: it needs ROS 2 and px4_msgs). It converts an ENU/FLU nav_msgs/Odometry to the NED/FRD VehicleOdometry. Verify field names and constants against the px4_msgs version you build.

import numpy as np, rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy, HistoryPolicy
from nav_msgs.msg import Odometry
from px4_msgs.msg import VehicleOdometry

def qmul(a, b):                                   # Hamilton product, [w, x, y, z]
    w1, x1, y1, z1 = a; w2, x2, y2, z2 = b
    return np.array([w1*w2 - x1*x2 - y1*y2 - z1*z2, w1*x2 + x1*w2 + y1*z2 - z1*y2,
                     w1*y2 - x1*z2 + y1*w2 + z1*x2, w1*z2 + x1*y2 - y1*x2 + z1*w2])

S = np.sqrt(0.5)
Q_NED_ENU = np.array([0.0, S, S, 0.0])            # 180 deg about (1,1,0)/sqrt(2): ENU -> NED
Q_FRD_FLU = np.array([0.0, 1.0, 0.0, 0.0])        # 180 deg about x: FLU -> FRD

class VioToPx4(Node):
    def __init__(self):
        super().__init__('vio_to_px4')
        qos = QoSProfile(reliability=ReliabilityPolicy.BEST_EFFORT, durability=DurabilityPolicy.TRANSIENT_LOCAL,
                         history=HistoryPolicy.KEEP_LAST, depth=1)
        self.pub = self.create_publisher(VehicleOdometry, '/fmu/in/vehicle_visual_odometry', qos)
        self.create_subscription(Odometry, '/vio/odometry', self.cb, 10)

    def cb(self, m: Odometry):
        o = VehicleOdometry()
        stamp_us = m.header.stamp.sec * 1_000_000 + m.header.stamp.nanosec // 1000     # sample time of the data
        o.timestamp = int(self.get_clock().now().nanoseconds // 1000)                  # publication time (see note below)
        o.timestamp_sample = stamp_us      # NOTE: ROS time and the autopilot clock must be synchronised, or convert with a measured offset
        o.pose_frame = VehicleOdometry.POSE_FRAME_FRD   # check the constant in your px4_msgs version
        p = m.pose.pose.position
        o.position = [float(p.y), float(p.x), float(-p.z)]                             # ENU -> NED
        q = m.pose.pose.orientation
        qn = qmul(qmul(Q_NED_ENU, np.array([q.w, q.x, q.y, q.z])), Q_FRD_FLU)          # q_NED<-FRD
        o.q = [float(v) for v in qn]
        cov = np.array(m.pose.covariance).reshape(6, 6)
        o.position_variance = [float(cov[1, 1]), float(cov[0, 0]), float(cov[2, 2])]   # N, E, D variances
        nan3 = [float('nan')] * 3; o.velocity = nan3; o.angular_velocity = nan3         # unknown: NaN, not zero
        self.pub.publish(o)

rclpy.init(); rclpy.spin(VioToPx4())
Frame-converting odometry bridge. Clock synchronisation between ROS time and the autopilot is your responsibility (lesson M7-T8).

Common engineering mistakes

Publishing with the arrival time instead of the sample time

EKF2 fuses against the wrong state: the symptom is jitter proportional to speed.

Reporting constant, optimistic covariance

After tracking loss the filter trusts a wrong pose; publish honest variances and raise them on degraded tracking.

Zero instead of NaN for unknown fields

Zero velocity is a claim, not an absence of data.

Leaving GNSS fusion enabled while testing GNSS-denied behaviour

EKF2 keeps using GNSS: the test proves nothing. Disable it with the right parameter and verify in the log.

Mismatched heading between vision and magnetometer

Two yaw sources fighting each other cause slow rotation; choose the yaw source deliberately.

Failure modes

Failure modes, symptoms and causes
Failure modeHow it shows upLikely cause
Mirrored or rotated trajectoryVehicle flies the wrong way when commandedENU/FLU vs NED/FRD or yaw convention error
Position jitter proportional to speedHover is fine, motion is shakyWrong or missing sample timestamp
EKF2 rejects visionLog shows repeated innovation failures; vehicle falls back to other sourcesVariances too small, large frame error or divergence of the VIO
Height driftAltitude walks awayVertical position not fused or conflicting height sources

Debugging approach

  1. Plot the vision position beside EKF2’s local position in the log: they should differ by a fixed transform only.
  2. Look at the estimator innovation test ratios and fusion flags (see the log-analysis lesson) to confirm vision is being accepted.
  3. Compare the message timestamps with the autopilot clock to measure latency and offset.
  4. Test the bridge in SITL with a simulated odometry source before any real flight.

Practical exercises

  1. Derive QNED←ENU as the quaternion of a 180∘180^{\circ} rotation about (1, 1, 0)/2(1,\,1,\,0)/\sqrt{2} and check it in code.
  2. Compute the position error from a 60 ms unmodelled delay at 8 m/s.
  3. Decide the EKF2_EV_CTRL bits for a vehicle that should use vision horizontal position and yaw but barometer height.
  4. Write a test that feeds a known ENU pose and asserts the NED values.

Interview questions

Write an answer in your own words, then reveal the model answer. Nothing is sent anywhere.

Question 1

What must you set correctly in a VehicleOdometry message for EKF2?

Model answer

The frame (NED/FRD), the sample timestamp (acquisition time), honest variances, and NaN for unknown fields.

Question 2

Why does a wrong timestamp make estimates jitter with speed?

Model answer

The measurement is fused against the wrong past state, so the error is roughly speed times the timing error, which grows with velocity.

Question 3

How do you test GNSS-denied behaviour safely?

Model answer

In SITL first, with GNSS fusion disabled via the EKF2 parameters, an external odometry source, and the logs checked to confirm that only vision is used.

References and further reading

Primary papers, official documentation and standards. Each claim above that is specific to a tool or version points here.

What’s next

Mark this lesson

Saved in this browser and synced to your account when you are signed in.

Practice this lesson

M6-L6: Feed external visual odometry to PX4

Publish odometry to EKF2.

Next lesson

M6-T11: Offboard control

Command a UAV in offboard mode with the setpoint-stream handshake, correct frames and a tested failsafe on stream loss.