Susi Server

Vehicular Fusion (Odometry-IMU)

Node ID: vehicularFusion · Role: Filter · Realtime config: no

Description

Vehicle dead-reckoning filter fusing wheel odometry (CAN bus speed) with IMU. Uses bicycle or differential drive model. Outputs 2D vehicle pose with heading, velocity, and UTM coordinates.

Algorithm notes

What the filter estimates

This node runs a planar sigma-point (unscented) Kalman filter for a road or field vehicle. The estimated state is two-dimensional position, heading (yaw), and a slowly varying gyroscope yaw-rate bias. Motion is propagated with a constant-velocity, constant-turn-rate kinematic model driven by wheel speed and yaw rate as control inputs, so there is no acceleration or pitch/roll state.

Positions are held in a local metric frame whose axes are northing and easting, anchored on the first usable position fix. The UTM zone is latched at that moment, so the published global latitude and longitude never jump at a zone boundary. Yaw is measured from north toward east. Gyroscope samples are interpreted in the sensor frame and brought into the vehicle frame through the mounting rotation and the turn-rate axis selection.

How each input is used

Wheel speed is the pacing input: one fused pose is published per wheel-speed sample, and no other stream can produce output on its own. Speed enters as the linear control input, optionally scaled by the estimated wheel-speed scale factor.

The IMU supplies yaw rate about the selected axis, and its magnitude is also used for slip detection and standstill bias updates. If IMU samples stop arriving for longer than the configured timeout, the node either switches to the wheel-derived turn rate (only if the wheel fallback is enabled and the wheel rate is flagged valid) or stops publishing entirely, rather than dead-reckoning on a stale rate.

GNSS provides the position update, and optionally a heading update from a dual-antenna solution. Nothing is published until the filter has initialized: it waits for a fix of acceptable quality, then takes the initial heading either from the receiver orientation or, more commonly, from course over ground once several consecutive samples exceed the minimum speed and agree with the wheel speed. Heading quality is checked independently of position quality, so a degraded moving-baseline solution is rejected even at an RTK-fixed position, and a good heading keeps correcting yaw straight through a position outage.

Optical pose can replace GNSS as the position source. Its frame is scaled and, when alignment is enabled, rotated onto the GNSS frame after both sensors have observed a couple of metres of motion; the first optical position is deliberately withheld until that rotation is known.

What the parameters control

Process noise on speed and turn rate sets how far the filter trusts dead reckoning between fixes: raise them for a responsive but noisier trajectory, lower them for a smooth trajectory that lags real accelerations and turns.

Measurement trust is set by the base position sigma plus the quality-dependent options. Quality weighting admits DGPS and float fixes at loose sigmas as a drift leash rather than treating them like an RTK fix, and correction-age de-weighting bleeds trust away from RTK fixes computed on stale corrections. Robustness parameters gate the position innovation; an outlier is softly de-weighted, never discarded.

Heading and calibration options select the yaw-rate source, enable the gyro bias state (learned from long straight segments with verified GNSS heading and from stopped samples), and enable the slow wheel-speed scale estimate learned from RTK-versus-odometry travelled distance.

Stopped detection freezes the state below the velocity threshold, with hysteresis, and smoothing blends the lagged and current states so fixes do not appear as visible steps.

Tuning and failure modes

Start with heading: verify the turn-rate axis and mounting rotation by driving a slow figure-eight and checking that reported yaw rate matches the turn direction. A wrong axis or sign produces heading that diverges immediately while position looks plausible for a few seconds.

Common problems: no output at all usually means initialization never completed (fix quality too low, or the vehicle never drove straight above the initialization speed); reversing during startup corrupts course-over-ground initialization; an overly tight innovation gate combined with very small position sigma makes the filter reluctant after long outages; loose float sigmas set too tight make the filter chase float noise; smoothing adds a small effective latency that matters for control loops; and wheel scale or slip estimates degrade if the wheel angular rate is unreliable. Playback seeks and loops re-anchor the filter instead of fusing across the time jump.

Inputs / Outputs

Config aliases

OdometryImuFilter, odometryImuFilter, vehicularFusion

Required feature

vehicular_fusion

Properties

The following fields are exposed in the node’s Properties panel in the UI. Types map to UI widgets (e.g. string → text input, number → numeric, boolean → toggle, select → dropdown, json → JSON editor).

KeyLabelTypeDefaultDescription / Notes
_h_coreTuningCore Tuningheading
fuser.velErrorVelocity Errornumber0.278Control noise std-dev on the wheel-speed input in m/s. Higher values trust the odometry speed less; lower values smooth speed but lag real accelerations.
fuser.omegaErrorAngular Velocity Errornumber0.5Control noise std-dev on the turn-rate input in rad/s. Controls how quickly the heading may change between updates. Lower values produce smoother heading but slower response to real turns.
fuser.measurementErrorMeasurement Errornumber0.1Measurement noise std-dev for the GNSS / optical position update, in meters. Lower values trust the position fixes more; higher values let odometry dead-reckoning carry more weight.
fuser.smoothFitSmoothingbooleantrueSmooth the output by interpolating between the lagged and current filter states across measurement epochs. Removes the visible position step at each fix at the cost of a small effective latency.
_h_outlierHandlingOutlier Handlingheading
fuser.usePositionOutlierDeweightingUse Position Outlier De-weightingbooleantrueWhen a position update exceeds the innovation gate, inflate its measurement noise and apply it softly instead of applying it at full weight.
fuser.innovationGateInnovation Gate (sigma)number5.0Chi-square threshold for enabled outlier handling: a fix whose squared Mahalanobis distance exceeds dof + gate x sqrt(2 x dof) is treated as an outlier. 0 disables position outlier de-weighting.
_h_wheelWheel Odometry & Turn Rateheading
fuser.useImuTurnRateUse IMU Turn RatebooleantrueTake the vehicle yaw rate from the IMU gyroscope (about the IMU Turn Rate Axis) instead of deriving it from differential wheel speeds. Recommended when a gyro is available, as it is far less sensitive to wheel slip and track-width error.
fuser.useWheelAngularFallbackWheel Turn-Rate FallbackbooleanfalseFall back to the wheel-derived turn rate when the IMU stream is lost. Off by default: with no measured wheel angular rate a dead IMU leaves no heading reference, so output is stopped rather than dead-reckoned on a stale turn rate. Enable only when a trustworthy wheel angular rate is available.
fuser.imuTimeoutLimitIMU Timeout (s)number0.5Time in seconds without an IMU sample before the IMU stream is declared lost. On loss the filter stops output - or switches to the wheel turn rate if Wheel Turn-Rate Fallback is on and a valid wheel rate is present. The IMU normally streams at 100 Hz; raise this only to tolerate longer dropouts.
fuser.imuTurnRateAxisIMU Turn Rate Axisvector3{"x":0,"y":0,"z":1}Unit vector selecting which IMU gyroscope axis (in the IMU frame) supplies the vehicle yaw rate. Default (0,0,1) uses the IMU Z axis; set to match the axis pointing up through the vehicle.
fuser.wheelOmegaScaleWheel Omega Scalenumber1.0Multiplier applied to the wheel-derived angular velocity to correct for track-width or wheel-radius calibration error. Only relevant when Use IMU Turn Rate is off. 1.0 leaves the wheel turn rate unscaled.
fuser.detectSlipDetect Wheel SlipbooleantrueInflate the control noise on ticks where the IMU and wheel-derived turn rates disagree (wheel slip or wrong track width).
fuser.estimateWheelScaleEstimate Wheel ScalebooleantrueEstimate a slow wheel-speed scale factor (tire wear/pressure) from the ratio of RTK-fixed travelled distance to integrated odometry distance.
fuser.estimateGyroBiasEstimate Gyro BiasbooleantrueEstimate the gyro yaw-rate bias as a Kalman filter state and subtract it from the IMU turn rate - the dominant yaw-drift source during GNSS outages. Verified multi-second GNSS-yaw windows and stopped gyro samples measure it directly.
fuser.gyroBiasRandomWalkGyro Bias Random Walknumber0.0005Random-walk process noise density for the odometry-stage gyro yaw-bias state, in rad/s per sqrt(s). Lets the in-run bias track slow thermal changes.
fuser.gyroBiasZuptErrorGyro Bias ZUPT Errornumber0.0035Measurement noise for stopped zero-angular-rate gyro-bias updates, in rad/s.
_h_orientationOrientation & Headingheading
fuser.initializeFromGnssOrientationInitialize from GNSS OrientationbooleanfalseSeed the filter’s initial heading from the GNSS dual-antenna orientation, avoiding the need to drive forward to resolve heading at startup. Only used when Use GNSS Without RTK Fix is off; when off without a valid reported orientation (single antenna), or when on, the heading is initialized from course over ground instead.
fuser.useGnssYawMeasurementUse GNSS Yaw MeasurementbooleanfalseUse the GNSS dual-antenna heading as a 1D yaw measurement while moving, instead of relying on position cross-covariance alone. Also keeps the heading corrected through a position-only outage and feeds the moving gyro-bias estimator.
fuser.gnssYawErrorGNSS Yaw Errornumber0.05GNSS heading measurement noise std-dev in radians.
_h_stoppedStopped Detectionheading
fuser.retainStateWhenStoppedRetain State When StoppedbooleantrueFreeze the filter state while the vehicle is detected as stationary, preventing position and heading drift from sensor noise when not moving.
fuser.velocityThresholdVelocity Thresholdnumber0.01Vehicle speed in m/s below which the vehicle is considered stopped (with hysteresis at half this value). Used for stopped-state detection and optional state freezing.
_h_initInitialization & Convergenceheading
fuser.initVelocityThresholdInit Velocity Thresholdnumber3.0Minimum speed in m/s for the course-over-ground heading initialization. Heading is locked in after several consecutive GNSS samples above this speed.
fuser.initVelocityToleranceInit Velocity Tolerancenumber0.4Maximum allowed difference in m/s between the GNSS-derived speed and the wheel speed for a sample to count toward the course-over-ground heading initialization.
_h_rtkRTK & GNSS Qualityheading
fuser.useGpsOnRtkFloatUse GNSS Without RTK FixbooleanfalseAccept GNSS position fixes of any quality instead of only RTK Fixed (quality 4). When off, the filter stays uninitialized until the first RTK Fixed sample; position accuracy degrades to that of the raw fixes when on.
fuser.useGnssQualityWeightingWeight GNSS by Fix QualitybooleanfalseWeight GNSS position fixes by their NMEA fix quality instead of the all-or-nothing RTK gate. DGPS and RTK-float fixes are admitted as a loose drift leash (using the sigmas below) that bounds dead-reckoning drift during an RTK outage without being chased like an RTK Fixed fix. Single-point fixes stay rejected. Supersedes Use GNSS Without RTK Fix when on.
fuser.gnssDgpsErrorDGPS Position Errornumber4.0Position measurement noise std-dev in meters for DGPS / SBAS fixes (quality 2). Only used when Weight GNSS by Fix Quality is on.
fuser.gnssFloatErrorRTK Float Position Errornumber12.0Position measurement noise std-dev in meters for RTK Float fixes (quality 5). Only used when Weight GNSS by Fix Quality is on.
fuser.useRtcmAgeDeweightingUse RTCM Age De-weightingbooleantrueInflate the GNSS position and heading measurement noise with the receiver’s RTCM correction age, so an RTK-fixed fix on stale corrections is smoothly de-weighted toward dead reckoning. When off, RTK-fixed fixes are trusted regardless of correction age.
fuser.rtcmAgeSigmaPerSecRTCM Age Sigma Per Second (m/s)number0.05Per-second growth in meters of the GNSS position sigma when RTK FIXED with a non-zero RTCM correction age. Effective measurement error becomes measurementError + diffAge * this. Only used when Use RTCM Age De-weighting is on.
_h_mountingIMU Mounting & Framesheading
fuser.imuToCarRotationIMU-to-Car Rotationquaternion{"w":1,"x":0,"y":0,"z":0}Quaternion (w,x,y,z) describing the rotation from the IMU sensor frame to the vehicle body frame. Accounts for the physical mounting orientation of the IMU.
_h_opticalOpticalheading
fuser.useOpticalDataUse Optical DatabooleanfalseIncorporate an attached optical pose stream as an additional position measurement, useful for indoor or GNSS-denied segments.
fuser.opticalScalingFactorOptical Scaling Factornumber10.0Scale factor applied to incoming optical position to convert it into meters / correct for tracking-volume scaling before fusion. Only used when Use Optical Data is enabled.
fuser.alignOpticalToGnssAlign Optical to GNSSbooleantrueRotate the optical coordinate frame to match the GNSS/world frame before fusing, so optical and GNSS positions share a common reference. Only used when Use Optical Data is enabled.

Example node definition

A full node definition in config.json looks like the block below. Paste it under the sinks key of your config, keyed by the node’s instance name (any identifier; it doesn’t have to match the node ID).

{
  "sinks": {
    "vehicularFusion": {
      "dataEndpoint": "inproc://vehicularFusion_data",
      "inputEndpoints": [
        "inproc://gnss_data",
        "inproc://imu_data",
        "inproc://vehicleSpeed_data",
        "inproc://optical_data"
      ],
      "inputDataFilter": [
        "Gnss",
        "Imu",
        "VehicleSpeed",
        "Optical"
      ],
      "settings": {
        "fuser": {
          "alignOpticalToGnss": true,
          "detectSlip": true,
          "estimateGyroBias": true,
          "estimateWheelScale": true,
          "gnssDgpsError": 4.0,
          "gnssFloatError": 12.0,
          "gnssYawError": 0.05,
          "gyroBiasRandomWalk": 0.0005,
          "gyroBiasZuptError": 0.0035,
          "imuTimeoutLimit": 0.5,
          "imuToCarRotation": {
            "w": 1,
            "x": 0,
            "y": 0,
            "z": 0
          },
          "imuTurnRateAxis": {
            "x": 0,
            "y": 0,
            "z": 1
          },
          "initVelocityThreshold": 3.0,
          "initVelocityTolerance": 0.4,
          "initializeFromGnssOrientation": false,
          "innovationGate": 5.0,
          "measurementError": 0.1,
          "omegaError": 0.5,
          "opticalScalingFactor": 10.0,
          "retainStateWhenStopped": true,
          "rtcmAgeSigmaPerSec": 0.05,
          "smoothFit": true,
          "useGnssQualityWeighting": false,
          "useGnssYawMeasurement": false,
          "useGpsOnRtkFloat": false,
          "useImuTurnRate": true,
          "useOpticalData": false,
          "usePositionOutlierDeweighting": true,
          "useRtcmAgeDeweighting": true,
          "useWheelAngularFallback": false,
          "velError": 0.278,
          "velocityThreshold": 0.01,
          "wheelOmegaScale": 1.0
        }
      }
    }
  }
}

Field reference

FieldPurpose
dataEndpointEndpoint this node binds to publish its output. Omit to let the runtime generate one.
inputEndpointsList of upstream endpoints this node connects to (outEndpoint / dataEndpoint values from upstream nodes).
inputDataFilterOptional whitelist of data types. Messages whose type isn’t in the list are dropped at the subscriber.
settingsAll user-configurable fields — see the Properties table above.

Connections can also be declared at the top-level connections array when settings.explicitConnections is true, as an alternative to filling inputEndpoints on each node.

Loading documentation…