Tuning a Multi-Object Tracker

This example shows how to tune and run a tracker to track multiple objects in the scene. The example explains and demonstrates the importance of key properties of the trackers in the Sensor Fusion and Tracking Toolbox.

Default Tracker Configuration

In order to test the capability of a tracker to track multiple objects, you set up a basic scenario. In the scenario, you define three objects with each moving along a straight line at a constant velocity. Initially, you set the object velocities to be 48 m/s, 60 m/s, and 72 m/s, respectively.

stopTime = 10;
v = 60;
scenario = trackingScenario;
scenario.StopTime = stopTime;
scenario.UpdateRate = 0;
p1 = platform(scenario);
p1.Trajectory = waypointTrajectory([30 20 0; 30 .8*v*stopTime 0], [0 stopTime]);
p2 = platform(scenario);
p2.Trajectory = waypointTrajectory([0 0 0; 0 v*stopTime 0], [0 stopTime]);
p3 = platform(scenario);
p3.Trajectory = waypointTrajectory([-30 -20 0; -30 1.2*v*stopTime 0], [0 stopTime]);

In addition, you define a radar that stares at the scene and updates 5 times per second. You mount it on a platform located on the side of the moving objects.

pRadar = platform(scenario);
pRadar.Trajectory = kinematicTrajectory('Position', [-v*stopTime 0.5*v*stopTime 0]);
radar = monostaticRadarSensor(1, 'No scanning', 'UpdateRate', 5, ...
    'MountingAngles', [0 0 0], 'AzimuthResolution', 1, ...
    'FieldOfView', [100 1], 'HasINS', true, 'DetectionCoordinates', 'Scenario', ...
    'MaxUnambiguousRange', 1000);
pRadar.Sensors = radar;

You create a theater plot to display the scene.

fig = figure;
ax = axes(fig);
tp = theaterPlot('Parent', ax, 'XLimits', [-11*v 100], 'YLimits', [-50 15*v], 'ZLimits', [-100 100]);
rp = platformPlotter(tp, 'DisplayName', 'Radar', 'Marker', 'd');
pp = platformPlotter(tp, 'DisplayName', 'Platforms');
dp = detectionPlotter(tp, 'DisplayName', 'Detections');
trp = trackPlotter(tp, 'DisplayName', 'Tracks', 'ConnectHistory', 'on', 'ColorizeHistory', 'on');
covp = coveragePlotter(tp, 'DisplayName', 'Radar Coverage', 'Alpha', [0.1 0]);

Finally, you create a default trackerGNN object, run the scenario, and observe the results. You use trackGOSPAMetric to evaluate the tracker performance.

tracker = trackerGNN;
tgm = trackGOSPAMetric("Distance","posabserr");
gospa = zeros(1,51); % number of timesteps is 51
i = 0;

% Define the random number generator seed for repeatable results
s = rng(2019); 
while advance(scenario)
    % Get detections
    dets = detect(scenario);
    
    % Update the tracker
    if isLocked(tracker) || ~isempty(dets)
        [tracks, ~, ~, info] = tracker(dets, scenario.SimulationTime);
    end
    
    % Evaluate GOSPA
    i = i + 1;
    truth = platformPoses(scenario);
    gospa(i) = tgm(tracks, truth);
    
    % Update the display
    updateDisplay(rp, pp, dp, trp, covp, scenario, dets, tracks);
end

rng(s)
figure
plot(gospa)
title('Generalized OSPA vs. Timestep')

You observe that the tracker was not able to track the three objects. At some point, additional tracks are confirmed and shown in addition to the three expected tracks for the three moving objects. As a result, the value of the GOSPA metric increases. Note that lower values of the GOSPA metric indicate better tracker performance.

Assignment Threshold

You look at the info struct that the tracker outputs and observe that the CostMatrix and the Assignments do not show an assignment of pairs of tracks and objects you expect to happen. This means that the AssignmentThreshold is too small and should be increased. Increase the assignment threshold to 50.

release(tracker);
tracker.AssignmentThreshold = 50;
rerunScenario(scenario, tracker, tgm, tp);

The assignment threshold property also has a maximum distance from a detection that you can set. This is the last element in the AssignmentThreshold value. You can use this value to speed the tracker up when handling large number of detections and tracks. Refer to the How to Efficiently Track Large Numbers of Objects example for more details. Here, you set the value to 2000 instead of inf, which will reduce the number of combinations of tracks and detections that are used to calculate the assignment cost.

release(tracker);
tracker.AssignmentThreshold = [50 2000];
rerunScenario(scenario, tracker, tgm, tp);

Filter Initialization Function

The results above show that the tracker is capable of maintaining three tracks for the three moving objects without creating false tracks. However, would the tracker's perforamnce hold if the objects move at a faster speed? To check that, you modify the scenario and increase the object speeds to be 160, 200, and 240 m/s, respectively.

v = 200;
p1.Trajectory = waypointTrajectory([30 0 0; 30 0.8*v*stopTime 0], [0 stopTime]);
p2.Trajectory = waypointTrajectory([0 0 0; 0 v*stopTime 0], [0 stopTime]);
p3.Trajectory = waypointTrajectory([-30 0 0; -30 1.2*v*stopTime 0], [0 stopTime]);
pRadar.Trajectory = kinematicTrajectory('Position', [-v*stopTime 0.5*v*stopTime 0]);
tp.XLimits = [-100-v*stopTime 300];
tp.YLimits = [-100 100+v*1.2*stopTime];
release(radar);
radar.MaxUnambiguousRange = 3000;
rerunScenario(scenario, tracker, tgm, tp);

As you probably expected, the tracker could not establish stable tracks of the three objects. A possible solution is to increase the AssignmentThreshold even further to allow tracks to be updated. However, this option may increase the chance of random false tracks being assigned to multiple detections and getting confirmed. So, you choose to modify how a filter is being initialized.

You set the FilterInitializationFcn property to the function initFastCVEKF. The funciton is the same as the default initcvekf except it increases the uncertainty in the velocity components of the state. It allows the initial state to account for larger unknown velocity values, but once the track establishes, the uncertainty decreases again.

release(tracker)
tracker.FilterInitializationFcn = @initFastCVEKF;
rerunScenario(scenario, tracker, tgm, tp);

You observe that the tracker is once again able to maintain the tracks, and there are 3 tracks for the three moving objects, even though they are moving faster now. The GOSPA value is also reduced after a few steps.

Confirmation and Deletion Thresholds

You want to make sure that your tracker is robust to a higher false alarm rate. To do that, you configure the radar to have a false alarm rate that is 250 times higher than previously.

You zoom out to view a larger portion of the scene and to see if false tracks are created.

release(radar);
radar.FalseAlarmRate = 2.5e-4;
tp.XLimits = [-2100 300];
tp.YLimits = [-100  3100];
tp.ZLimits = [-1000 1000];
rerunScenario(scenario, tracker, tgm, tp);

There are a few false tracks being created (not all of them shown) and they increase the GOSPA value.

You want to delete false tracks more quickly, for which you use the DeletionThreshold property. The default value for deletion threshold is [5 5], which requires 5 consecutive misses before a confirmed track is deleted. You can make the deletion process quicker by reducing this value to [3 3], or 3-out-of-3 misses. Alternatively, since the radar DetectionProbability is high, you can even delete a track after 2-out-of-3 misses, by setting:

release(tracker)
tracker.DeletionThreshold = [2 3];
rerunScenario(scenario, tracker, tgm, tp);

As expected, reducing the number of steps it takes to delete tracks decreases the GOSPA value because these false tracks are short-lived. However, these false tracks are still confirmed and deteriorate the overall tracking quality.

So, you want to make the confirmation of new tracks more stringent in attempt to reduce the number of false tracks. Consider the tracker ConfirmationThreshold property.

disp(tracker.ConfirmationThreshold);
     2     3

The value shows that the tracker confirms every track if it is assigned two detections in the first three updates. You decide to make it harder to confirm a track and reset the value to be 3-out-of-4 assignments.

release(tracker)
tracker.ConfirmationThreshold = [3 4];
rerunScenario(scenario, tracker, tgm, tp);

By making track confirmation more stringent you were able to eliminate the false tracks. As a result, the GOSPA values are down to around 20 again, except for the first few steps.

Maximum Number of Tracks

Making the confirmation more stringent allowed you to eliminate false tracks. However, the tracker still initializes a track for each false detection, which you can observe by looking at the number of tracks the tracker currently maintains.

The number of tracks fluctuates through the lifetime of the tracker. However, you don't want it to exceed the maximum number of tracks that the tracker can maintain, defined by MaxNumTracks, which is by default 100. If the tracker exceeds this number, it issues a warning that a new track could not be added, but the execution can continue.

Increase the maximum number of tracks to allow the tracker to track even with a higher false alarm rate without issuing the warning. You also want to make track confirmation even more stringent to reduce the number of false tracks and reduce the GOSPA metric.

release(tracker)
tracker.MaxNumTracks = 200;
tracker.ConfirmationThreshold = [5 6];

release(radar)
radar.FalseAlarmRate = 1e-3;
rerunScenario(scenario, tracker, tgm, tp);

Note that making track confirmation more stringent requires more steps to confirm tracks. As a result, for the first few steps, there are no confirmed tracks and the GOSPA value remains high. You can compare that with the GOSPA graphs in the beginning of this example.

Try Another Tracker

You want to try using another tracker, in this case, the Joint Probabilistic Data Association tracker, trackerJPDA. Many of the properties defined for the previous tracker are still valid in the JPDA tracker. You use the helperGNN2JPDA to construct the JPDA tracker.

jpda = helperGNN2JPDA(tracker);
jpda.ClutterDensity = 1e-3;
rerunScenario(scenario, tracker, tgm, tp);

Summary

In this example, you learned to set up multi-object trackers in order to maintain tracks of the real objects in the scene, avoid false tracks, maintain the right number of tracks, and switch between different trackers.

When tuning a multi-object tracker, consider changing the following properties:

FilterInitializationFcn — defines the filter used to initialize a new track with an unassigned detection.

  • The function defines the motion and measurement models.

  • Check that the state uncertainties are correctly represented by the StateCovariance of the filter. For example, a large initial velocity covariance allows tracking fast moving objects.

AssignmentThreshold — maximum normalized distance used in the assignment of detections to tracks.

  • Decrease this value to speed up assignment, especially in large scenes with many tracks and to avoid false tracks, redundant tracks, and track swaps. If using a trackerJPDA, reducing this value reduces the number of detections that are considered as assigned to a single track. If using a trackerTOMHT, reducing the assignment thresholds reduces the number of track branches that are created.

  • Increase this value to avoid tracks diverging and breaking even in the presence of new detections.

MaxNumTracks — maximum number of tracks maintained by the tracker.

  • Decrease this value to minimize memory usage and speed up tracker initialization.

  • Increase this value to avoid new tracks not being initialized because the limit was reached.

ConfirmationThreshold — controls the confirmation of new tracks.

  • Decrease this value to confirm more tracks and to confirm tracks more quickly.

  • Increase this value to lower the number of false tracks.

DeletionThreshold — controls the coasting and deletion of tracks.

  • Decrease this value to delete tracks more quickly (for example when the probability of detection is high).

  • Increase this value to compensate for a lower probability of detection (for example coast tracks during occlusions).

You can refer to the following examples for additional details: How to Generate C Code for a Tracker, Introduction to Track Logic, Tracking Closely Spaced Targets Under Ambiguity, and Track Point Targets in Dense Clutter Using GM-PHD Tracker.

Utility Functions

updateDisplay

This function updates the display at every step of the simulation.

function updateDisplay(rp, pp, dp, trp, covp, scenario, dets, tracks)
    % Plot the platform positions
    poses = platformPoses(scenario);
    pos = reshape([poses(1:3).Position], 3, [])'; % Only the platforms
    plotPlatform(pp, pos);
    radarPos = poses(4).Position; % Only the radar
    plotPlatform(rp, radarPos)
    
    % Plot the detection positions
    if ~isempty(dets)
        ds = [dets{:}];
        dPos = reshape([ds.Measurement], 3, [])';
    else
        dPos = zeros(0,3);
    end
    plotDetection(dp, dPos);
    
    % Plot the tracks
    tPos = getTrackPositions(tracks, [1 0 0 0 0 0; 0 0 1 0 0 0; 0 0 0 0 1 0]);
    tIDs = string([tracks.TrackID]);
    plotTrack(trp, tPos, tIDs);
    
    % Plot the coverage
    covcon = coverageConfig(scenario);
    plotCoverage(covp, covcon);
end

rerunScenario

This function runs the scenario with a given tracker and a track GOSPA metric object. It uses the theater plot object to display the results.

function info = rerunScenario(scenario, tracker, tgm, tp)
    % Reset the objects after the previous run
    restart(scenario);
    reset(tracker);
    reset(tgm);
    rp = findPlotter(tp, 'DisplayName', 'Radar');
    pp = findPlotter(tp, 'DisplayName', 'Platforms');
    trp = findPlotter(tp, 'DisplayName', 'Tracks');
    dp = findPlotter(tp, 'DisplayName', 'Detections');
    covp = findPlotter(tp, 'DisplayName', 'Radar Coverage');
    clearPlotterData(tp);
    
    gospa = zeros(1,51); % number of timesteps is 51
    i = 0;
    s = rng(2019); 
    while advance(scenario)
        % Get detections
        dets = detect(scenario);

        % Update the tracker
        if isLocked(tracker) || ~isempty(dets)
            [tracks, ~, ~, info] = tracker(dets, scenario.SimulationTime); 
        end
        
        % Evaluate GOSPA
        i = i + 1;
        truth = platformPoses(scenario);
        gospa(i) = tgm(tracks, truth);

        % Update the display
        updateDisplay(rp, pp, dp, trp, covp, scenario, dets, tracks);
    end
    rng(s)
    figure
    plot(gospa(gospa>0))
    title('Generalized OSPA vs. Timestep')
end

initFastCVEKF

This function modifies the default initcvekf filter initialization function to allow for more uncertainty in the object speed.

function ekf = initFastCVEKF(detection)
    ekf = initcvekf(detection);
    initialCovariance = diag(ekf.StateCovariance);
    initialCovariance([2,4,6]) = 300^2; % Increase the speed covariance
    ekf.StateCovariance = diag(initialCovariance);
end

helperGNN2JPDA

This function provides the JPDA tracker equivalent to the GNN tracker given as an input.

function jpda = helperGNN2JPDA(gnn)
    jpda = trackerJPDA(...
        'MaxNumTracks', gnn.MaxNumTracks, ...
        'AssignmentThreshold', gnn.AssignmentThreshold, ...
        'FilterInitializationFcn', gnn.FilterInitializationFcn, ...
        'MaxNumSensors', gnn.MaxNumSensors, ...
        'ConfirmationThreshold', gnn.ConfirmationThreshold, ...
        'DeletionThreshold', gnn.DeletionThreshold, ...
        'HasCostMatrixInput', gnn.HasCostMatrixInput, ...
        'HasDetectableTrackIDsInput', gnn.HasDetectableTrackIDsInput);

    if strcmpi(gnn.TrackLogic, 'History')
        jpda.TrackLogic = 'History';
    else
        jpda.TrackLogic = 'Integrated';
        jpda.DetectionProbability = gnn.DetectionProbability;
        jpda.ClutterDensity = gnn.FalseAlarmRate / gnn.Volume;
    end
end