How to Efficiently Track Large Numbers of Objects

This example shows how to use the trackerGNN to track large numbers of targets. Similar techniques can be applied to the trackerJPDA and trackerTOMHT as well.

Introduction

In many applications, trackers are required to track hundreds or thousands of objects. Increasing the number of tracks maintained by a tracker is a challenge, caused by the computational complexity of the algorithm at the core of every tracker. In particular, two common stages in the tracker update step are not easily scalable: calculating the assignment cost and performing the assignment. Assignment cost calculation is common to trackerGNN, trackerJPDA, and trackerTOMHT, and the techniques shown in this example can be applied when using any of these trackers. The way each tracker performs the assignment is unique to each tracker, and may require tailored solutions to improve the tracker performance, which are beyond the scope of this example.

Scenario

For the purposes of this example, you define a scenario that contains 900 platforms, organized in a 15-by-15 grid, with each grid cell containing 4 platforms. The purpose of the grid cells is to demonstrate the benefit of coarse cost calculation, explained later in the example.

The following code arranges the 900 objects in the grid cell and creates the visualization. On the left, the entire scenario is shown. On the right, the visualization zooms in on 4 grid cells. Note each cell contains 4 platforms.

[platforms,tp,zoomedtp] = createPlatforms;

Use the Default Assignment Cost Calculation

This section shows the results of tracking the platforms defined above using a trackerGNN with default AssignmentThreshold. The AssignmentThreshold property contains two values: [C1 C2], where C1 is the threshold used for the assignment and C2 is a threshold for coarse calculation explained in the next section.

When the tracker is updated with a new set of detections, it calculates the cost of assigning every detection to every track. The accurate cost calculation must take into account the measurement and uncertainty of each detection as well as the expected measurement and expected uncertainty from each track, as depicted below.

By default, C2 is set to Inf, which requires that the costs of all combinations of track and detection are calculated. This leads to a more accurate assignment, but is more computationally intensive. You should start with the default setting to make sure the tracker assigns detections to tracks in the best way, and then consider lowering the value of C2 to reduce the time required for calculating the assignment cost.

During assignment cost calculation, elements of the cost matrix whose values are higher than C1 are replaced with Inf. Doing so helps the assignment algorithm to ignore impossible assignments.

Define a tracker that can track up to 1000 tracks. The tracker uses the default constant velocity extended Kalman filter, and its state is defined as [x;vx;y;vy;z;vz], which is used by the positionSelector below to get the position components.

tracker = trackerGNN('MaxNumTracks',1000, 'AssignmentThreshold', [30 Inf]);
positionSelector = [1 0 0 0 0 0; 0 0 1 0 0 0; 0 0 0 0 0 0];

On the first call to step, the tracker instantiates all tracks. To isolate the time required to instantiate the tracks from the processing time required for the first step, you can call setup and reset before stepping the tracker. See the supporting function runTracker at the end of this example for more details.

[trkSummary,truSummary,info] = runTracker(platforms,tracker,positionSelector,tp,zoomedtp);
Tracker set up time: 8.3108
Step 1 time: 3.7554
Step 2 time: 15.3029
Step 3 time: 14.1099
Step 4 time: 14.3506
Step 5 time: 14.3963

All steps from now are without detections.
Step 6 time: 0.53103
Step 7 time: 0.52582
Step 8 time: 0.50639
Step 9 time: 0.50909
Step 10 time: 0.16034
Scenario done. All tracks are now deleted.

You analyze the tracking results by examining the track assignment metrics values. For perfect tracking the total number of tracks should be equal to the number of platforms and there should be no false, swapped, or divergent tracks. Similarly, there should be no missing truth or breaks in the truth summary.

assignmentMetricsSummary(trkSummary,truSummary)
Track assignment metrics summary:
    TotalNumTracks    NumFalseTracks    MaxSwapCount    MaxDivergenceCount    MaxDivergenceLength
    ______________    ______________    ____________    __________________    ___________________

         900                0                0                  0                      0         

Truth assignment metrics summary:
    TotalNumTruths    NumMissingTruths    MaxEstablishmentLength    MaxBreakCount
    ______________    ________________    ______________________    _____________

         900                 0                      1                     0      

Use Coarse Assignment Cost Calculation

In the previous section, you saw that the tracker is able to track all the platforms, but every update step takes a long time. Most of the time was spent on calculating the assignment cost matrix.

Examining the cost matrix, you can see that vast majority of its elements are, in fact, Inf.

cm = info.CostMatrix;
disp("Cost matrix has " + numel(cm) + " elements.");
disp("But the number of finite values is " + numel(cm(isfinite(cm))) + newline)
Cost matrix has 810000 elements.
But the number of finite values is 2700

The above result shows that the cost calculation spends too much time on calculating the assignment cost of all the combinations of track and detection. However, most of these combinations are too far to be assigned, as the actual measurement is too far from the track expected measurement based on the sensor characteristics. To avoid the waste in calculating all the costs, you can use coarse cost calculation.

Coarse calculation is done to verify which combinations of track and detection may require an accurate normalized distance calculation. Only combinations whose coarse assignment cost is lower than C2 are calculated accurately. The coarse cost calculation is depicted in the image below. A detection is represented by its measurement and measurement noise . Two tracks are predicted to the time of the detection and projected to the measurement space, depicted by the points and . Note that the track uncertainty is not projected to the measurement space, which allows us to vectorize the coarse calculation. This is a rough estimate, because only the uncertainty around the detection is taken into account. In the depicted example, the first track falls outside of the coarse calculation gate while the second track falls inside it. Thus, accurate cost calculation is only done for the combination of this detection and the second track.

To use coarse cost calculation, release the tracker and modify its AssignmentThreshold to a value of [30 200]. Then, rerun the tracker.

release(tracker)
tracker.AssignmentThreshold = [30 200];
[trkSummary,truSummary] = runTracker(platforms,tracker,positionSelector,tp,zoomedtp);
Tracker set up time: 6.5846
Step 1 time: 3.5863
Step 2 time: 3.4095
Step 3 time: 2.9347
Step 4 time: 2.8555
Step 5 time: 2.9397

All steps from now are without detections.
Step 6 time: 0.51446
Step 7 time: 0.52277
Step 8 time: 0.54865
Step 9 time: 0.50941
Step 10 time: 0.19085
Scenario done. All tracks are now deleted.

You observe that the steps 3-5 now require significantly less time to complete. Step 2 is also faster than it used to be, but still slower than steps 3-5.

To understand why step 2 is slower, consider the track states after the first tracker update. The states contain position information, but the velocity is still zero. When the tracker calculates the assignment cost, it predicts the track states to the detection times, but since the tracks have zero velocity, they remain in the same position. This results in large distances between the detection measurements and the expected measurements from the predicted track states. These relatively large assignment costs make it harder for the assignment algorithm to find the best assignment, which causes step 2 to take more time than steps 3-5.

It's important to verify that the track assignment with coarse cost calculation remains the same as without it. If the track assignment metrics are not the same, you must increase the size of the coarse calculation gate. The following shows that the tracking is still perfect as it was in the previous section, but each processing step took less time.

assignmentMetricsSummary(trkSummary,truSummary)
Track assignment metrics summary:
    TotalNumTracks    NumFalseTracks    MaxSwapCount    MaxDivergenceCount    MaxDivergenceLength
    ______________    ______________    ____________    __________________    ___________________

         900                0                0                  0                      0         

Truth assignment metrics summary:
    TotalNumTruths    NumMissingTruths    MaxEstablishmentLength    MaxBreakCount
    ______________    ________________    ______________________    _____________

         900                 0                      1                     0      

Use an External Cost Calculation

Another way to control the time it takes to calculate the cost assignment is by using your own assignment cost calculation instead of the default the tracker uses.

An external cost calculation can take into account attributes that are not part of the track state and expected measurement. It can also use different distance metrics, for example Euclidean norm instead of normalized distance. The choice of which cost calculation to apply depends on the specifics of the problem, the measurement space, and how you define the state and measurement.

To use an external cost calculation, you release the tracker and set its HasCostMatrixInput property to true. You must pass your own cost matrix as an additional input with each update to the tracker. See the supporting function runTracker for more details.

release(tracker);
tracker.HasCostMatrixInput = true;
[trkSummary,truSummary] = runTracker(platforms,tracker,positionSelector,tp,zoomedtp);
assignmentMetricsSummary(trkSummary,truSummary)
Tracker set up time: 6.559
Step 1 time: 3.4394
Step 2 time: 1.7852
Step 3 time: 1.474
Step 4 time: 1.5312
Step 5 time: 1.5152

All steps from now are without detections.
Step 6 time: 0.60809
Step 7 time: 0.61374
Step 8 time: 0.616
Step 9 time: 0.63798
Step 10 time: 0.22762
Scenario done. All tracks are now deleted.

Track assignment metrics summary:
    TotalNumTracks    NumFalseTracks    MaxSwapCount    MaxDivergenceCount    MaxDivergenceLength
    ______________    ______________    ____________    __________________    ___________________

         900                0                0                  0                      0         

Truth assignment metrics summary:
    TotalNumTruths    NumMissingTruths    MaxEstablishmentLength    MaxBreakCount
    ______________    ________________    ______________________    _____________

         900                 0                      1                     0      

As expected, the processing time is even lower when using an external cost calculation function.

Change the GNN Assignment Algorithm

Another option to try is using a different GNN assignment algorithm that may be more efficient in finding the assignment by modifying the Assignment property of the tracker.

release(tracker)
tracker.Assignment = 'Jonker-Volgenant';
tracker.HasCostMatrixInput = true;
runTracker(platforms,tracker,positionSelector,tp,zoomedtp);
Tracker set up time: 6.494
Step 1 time: 3.5346
Step 2 time: 1.894
Step 3 time: 3.1192
Step 4 time: 3.1212
Step 5 time: 3.1458

All steps from now are without detections.
Step 6 time: 0.61109
Step 7 time: 0.62456
Step 8 time: 0.61849
Step 9 time: 0.60604
Step 10 time: 0.22303
Scenario done. All tracks are now deleted.

The Jonker-Volgenant algorithm performs the assignment in the second step faster relative to the default Munkres algorithm.

Monte-Carlo Simulation

If you want to run multiple scenarios without modifying the tracker settings, there is no need to call the release method. Instead, just call the reset method to clear previous track information from the tracker. This way, you save the time required to instantiate all the tracks. Note the "Tracker set up time" below relative to previous runs.

reset(tracker)
runTracker(platforms,tracker,positionSelector,tp,zoomedtp);
Tracker set up time: 0.097531
Step 1 time: 3.4684
Step 2 time: 1.6592
Step 3 time: 3.1429
Step 4 time: 3.1274
Step 5 time: 3.0994

All steps from now are without detections.
Step 6 time: 0.63232
Step 7 time: 0.61857
Step 8 time: 0.61433
Step 9 time: 0.60698
Step 10 time: 0.25301
Scenario done. All tracks are now deleted.

Summary

This example showed how to track large numbers of objects. When tracking many objects, the tracker spends a large fraction of the processing time on computing the cost assignment for each combination of track and detection. You saw how to use the cost calculation threshold to improve the time spent on calculating the assignment cost. In addition, the example showed how to use an external cost calculation, which may be designed to be more computationally efficient for the particular tracking problem you have.

You can reduce the cost assignment threshold or use an external cost calculation to improve the speed of the trackerJPDA and the trackerTOMHT as well.

Supporting Functions

createPlatforms

This function creates the platforms in a 20x20 grid with 2x2 platforms per grid cell.

function [platforms,tp,zoomedtp] = createPlatforms
% This is a helper function to run the tracker and display the results. It
% may be removed in the future.
nh  = 15; % Number of horizontal grid cells
nv  = 15; % Number of vertical grid cells
nsq = 2;  % 2x2 platforms in a grid cell
nPl = nh*nv*nsq^2; % Overall number of platforms
xgv = sort(-50 + repmat(100 * (1:nh), [1 nsq]));
ygv = sort(-50 + repmat(100 * (1:nv), [1 nsq]));
[X,Y] = meshgrid(xgv,ygv);

npts = nsq/2;
xshift = 10*((-npts+1):npts) -5;
yshift = xshift;

xadd = repmat(xshift, [1 nh]);
yadd = repmat(yshift, [1 nv]);

[Xx, Yy] = meshgrid(xadd,yadd);

X = X + Xx;
Y = Y + Yy;
pos = [X(:),Y(:),zeros(numel(X),1)];

% The following creates an array of struct for the platforms, which are
% used later for track assignment metrics.
vel = [3 1 0]; % Platform velocity
platforms = repmat(struct('PlatformID', 1, 'Position', [0 0 0], 'Velocity', vel),nPl,1);
for i = 1:nPl
    platforms(i).PlatformID  = i;
    platforms(i).Position(:) = pos(i,:);
end

% Visualization
f = figure('Position',[1 1 1425 700]);
movegui center;
h1 = uipanel(f,'FontSize',12,'Position',[.01 .01 .48 .98],"Title","Scene View");
a1 = axes(h1,'Position',[0.05 0.05 0.9 0.9]);
tp = theaterPlot('Parent', a1, 'XLimits',[0 nh*100], 'YLimits',[0 nv*100]);
set(a1,'XTick',0:100:nh*100)
set(a1,'YTick',0:100:nv*100)
grid on
pp = trackPlotter(tp,'Tag','Truth','Marker','^','MarkerEdgeColor','k','MarkerSize',4,'HistoryDepth',10);
plotTrack(pp,reshape([platforms.Position],3,[])');
trackPlotter(tp, 'Tag','Tracks','MarkerEdgeColor','b','MarkerSize',6,'HistoryDepth',10);
c = get(a1.Parent,'Children');
for i = 1:numel(c)
    if isa(c(i),'matlab.graphics.illustration.Legend')
        set(c(i),'Visible','off')
    end
end

h2 = uipanel(f,'FontSize',12,'Position',[.51 .01 .48 .98],'Title','Zoomed View');
a2 = axes(h2,'Position',[0.05 0.05 0.9 0.9]);
zoomedtp = theaterPlot('Parent', a2, 'XLimits',[400 500], 'YLimits',[400 500]);
set(a2,'XTick',400:100:500)
set(a2,'YTick',400:100:500)
grid on
zoomedpp = trackPlotter(zoomedtp,'DisplayName','Truth','Marker','^','MarkerEdgeColor','k','MarkerSize',6,'HistoryDepth',10);
plotTrack(zoomedpp,reshape([platforms.Position],3,[])');
trackPlotter(zoomedtp, 'DisplayName','Tracks','MarkerEdgeColor','b','MarkerSize',8,'HistoryDepth',10,'ConnectHistory','on','FontSize',1);
end

runTracker

This function runs the tracker, updates the plotters, and gathers track assignment metrics.

function [trkSummary,truSummary,info] = runTracker(platforms,tracker,positionSelector,tp,zoomedtp)
% This is a helper function to run the tracker and display the results. It
% may be removed in the future.

pp = findPlotter(tp,'Tag','Truth');
trp = findPlotter(tp, 'Tag','Tracks');
zoomedpp = findPlotter(zoomedtp,'DisplayName','Truth');
zoomedtrp = findPlotter(zoomedtp, 'DisplayName','Tracks');

% To save time, pre-allocate all the detections and assign them on the fly.
nPl = numel(platforms);
det = objectDetection(0,[0;0;0]);
dets = repmat({det},[nPl,1]);

% Define a track assignment metrics object.
tam = trackAssignmentMetrics;

% Bring the visualization back.
set(tp.Parent.Parent.Parent,'Visible','on')

hasExternalCostFunction = tracker.HasCostMatrixInput;

% Measure the time it takes to set the tracker up.
tic
if ~isLocked(tracker)
    if hasExternalCostFunction
        setup(tracker,dets,0,0);
    else
        setup(tracker,dets,0);
    end
end
reset(tracker)
disp("Tracker set up time: " + toc);

% Run 5 steps with detections for all the platforms.
for t = 1:5
    for i = 1:nPl
        dets{i}.Time = t;
        dets{i}.Measurement = platforms(i).Position(:);
    end

    tic
    if hasExternalCostFunction
        if isLocked(tracker)
            % Use predictTracksToTime to get all the predicted tracks.
            allTracks = predictTracksToTime(tracker,'all',t);
        else
            allTracks = [];
        end
        costMatrix = predictedEuclidean(allTracks,dets,positionSelector);
        [tracks,~,~,info] = tracker(dets,t,costMatrix);
    else
        [tracks,~,~,info] = tracker(dets,t);
    end
    trPos = getTrackPositions(tracks, positionSelector);
    trIDs = string([tracks.TrackID]');
    disp("Step " + t + " time: " + toc)

    % Update the plot.
    plotTrack(pp,reshape([platforms.Position],3,[])');
    plotTrack(trp,trPos);
    plotTrack(zoomedpp,reshape([platforms.Position],3,[])');
    plotTrack(zoomedtrp,trPos,trIDs);
    drawnow

    % Update the track assignment metrics object.
    if nargout
        [trkSummary, truSummary] = tam(tracks,platforms);
    end

    % Update the platform positions.
    for i = 1:nPl
        platforms(i).Position = platforms(i).Position + platforms(i).Velocity;
    end
end
snapnow

% Run steps with no detections until the tracker deletes all the tracks.
disp("All steps from now are without detections.")
while ~isempty(tracks)
    t = t+1;
    tic
    if hasExternalCostFunction
        allTracks = predictTracksToTime(tracker,'all',t);
        costMatrix = predictedEuclidean(allTracks,{},positionSelector);
        tracks = tracker({},t,costMatrix);
    else
        tracks = tracker({},t);
    end
    disp("Step " + t + " time: " + toc)

    % Update the position of the tracks to plot.
    trPos = getTrackPositions(tracks,positionSelector);
    trIDs = string([tracks.TrackID]');

    % Update the plot.
    plotTrack(pp,reshape([platforms.Position],3,[])');
    plotTrack(trp,trPos);
    plotTrack(zoomedpp,reshape([platforms.Position],3,[])');
    plotTrack(zoomedtrp,trPos,trIDs);
    drawnow

    % Update the platform positions.
    for i = 1:nPl
        platforms(i).Position = platforms(i).Position + platforms(i).Velocity;
    end
end
disp("Scenario done. All tracks are now deleted." + newline)
clearData(pp)
clearData(trp)
clearData(zoomedpp)
clearData(zoomedtrp)
set(tp.Parent.Parent.Parent,'Visible','off') % Prevent excessive snapshots
drawnow
end

predictedEuclidean

The function calculates the Euclidean distance between measured positions from detections and predicted positions from tracks.

function euclidDist = predictedEuclidean(tracks,detections,positionSelector)
% This is a helper function to run the tracker and display the results. It
% may be removed in the future.

if isempty(tracks) || isempty(detections)
    euclidDist = zeros(numel(tracks),numel(detections));
    return
end

predictedStates = [tracks.State];
predictedPositions = positionSelector * predictedStates;
dets = [detections{:}];
measuredPositions = [dets.Measurement];
euclidDist = zeros(numel(tracks),numel(detections));
for i = 1:numel(detections)
    diffs = bsxfun(@minus, predictedPositions',measuredPositions(:,i)');
    euclidDist(:,i) = sqrt(sum((diffs .* diffs),2));
end
end

assignmentMetricsSummary

The function displays the key assignment metrics in a table form.

function assignmentMetricsSummary(trkSummary,truSummary)
trkSummary = rmfield(trkSummary, {'TotalSwapCount','TotalDivergenceCount',...
    'TotalDivergenceLength','MaxRedundancyCount','TotalRedundancyCount',...
    'MaxRedundancyLength','TotalRedundancyLength'});
truSummary = rmfield(truSummary, {'TotalEstablishmentLength','TotalBreakCount',...
    'MaxBreakLength','TotalBreakLength'});
trkTable = struct2table(trkSummary);
truTable = struct2table(truSummary);
disp("Track assignment metrics summary:")
disp(trkTable)
disp("Truth assignment metrics summary:")
disp(truTable)
end