Comparing Efficient Hypothesis Management (EHM) with probability associators

In this example, we compare the performances between efficient hypothesis management (EHM) and standard joint probabilistic data association. The problem we face when dealing with multi-target tracking is the potential association of measurements to predictions, which can be ambiguous and that could lead to a combinatorial explosion. To reduce the computational cost of the operations, a number of algorithms have been developed to match measurements and predictions to tracks. One of these methods is the efficient hypothesis management, explained in detail in [1], [2] and under patent in [3]; this algorithm improves the joint probability data association, which is a brute force approach, with improved capability of hypothesis matching and rejection with, significantly, cost reduction.

A plugin Stone Soup implementation of EHM is available, under patent license agreement, using the Python package PyEHM developed by Dr. Lyudmil Vladimirov ‘PyEHM <https://github.com/sglvladi/pyehm>`_.

This example follows the usual setup: 1) Generate a simple multi-target scenario simulation; 2) Prepare the trackers components with the different data associators; 3) Run the trackers to collect the tracks; 4) Compare the trackers performances;

1) Generate a simple multi-target scenario simulation;

To start with this example we align a typical case of multi-target scenario with some level of clutter. Then, we set up the various components of the trackers. To use this example there is the need to install the independent package PyEHM using “pip install pyehm”.

General imports

import numpy as np
from datetime import datetime, timedelta
from copy import deepcopy
from time import perf_counter

# Load the pyehm plugins
from stonesoup.plugins.pyehm import JPDAWithEHM, JPDAWithEHM2

Stone Soup Imports

from stonesoup.types.array import StateVector, CovarianceMatrix
from stonesoup.types.state import GaussianState
from stonesoup.models.transition.linear import (
    CombinedLinearGaussianTransitionModel, ConstantVelocity)

Simulation parameters setup

np.random.seed(1908)  # set the random seed for the simulation
simulation_start_time = datetime.now().replace(microsecond=0)  # simulation start

# initial state of all targets
initial_state_mean = StateVector([0, 0, 0, 0])
initial_state_covariance = CovarianceMatrix(np.diag([5, 0.5, 5, 0.5]))
timestep_size = timedelta(seconds=1)
number_of_steps = 50  # number of time-steps
birth_rate = 0.25   # probability of new target to appear
death_probability = 0.01  # 1% probability of target to disappear

# set up the initial state of the simulation
initial_state = GaussianState(state_vector=initial_state_mean,
                              covar=initial_state_covariance,
                              timestamp=simulation_start_time)

# create the targets transition model
transition_model = CombinedLinearGaussianTransitionModel(
    [ConstantVelocity(0.05), ConstantVelocity(0.05)])

# Put this all together in a multi-target simulator.
from stonesoup.simulator.simple import MultiTargetGroundTruthSimulator
groundtruth_sim = MultiTargetGroundTruthSimulator(
    transition_model=transition_model,
    initial_state=initial_state,
    timestep=timestep_size,
    number_steps=number_of_steps,
    birth_rate=birth_rate,
    death_probability=death_probability)

# Load the measurement model
from stonesoup.models.measurement.linear import LinearGaussian

# initialise the measurement model
measurement_model_covariance = np.diag([0.5, 0.5])
measurement_model = LinearGaussian(4,
                                   [0, 2],
                                   measurement_model_covariance)

# probability of detection
probability_detection = 0.99

Generate clutter

clutter_area = np.array([[-1, 1], [-1, 1]])*30
surveillance_area = ((clutter_area[0][1]-clutter_area[0][0])*
                     (clutter_area[1][1]-clutter_area[1][0]))
clutter_rate = 1.2
clutter_spatial_density = clutter_rate/surveillance_area

# Instantiate the detection simulator
from stonesoup.simulator.simple import SimpleDetectionSimulator

detection_sim = SimpleDetectionSimulator(
    groundtruth=groundtruth_sim,
    measurement_model=measurement_model,
    detection_probability=probability_detection,
    meas_range=clutter_area,
    clutter_rate=clutter_rate)

# To make a 1 to 1 comparison between different trackers we have
# to feed the same detections to each tracker, so we have to
# duplicate the detection simulations.
from itertools import tee
detection, *detection_sims = tee(detection_sim, 4)

2) Prepare the trackers components with the different data associators;

We have set up the multi-target scenario, we instantiate all the relevant tracker components. We consider the UnscentedKalmanPredictor and UnscentedKalmanUpdater components for the tracker. Then, for the data association we use the JPDA data associator implementation present in Stone Soup and the JPDA PyEHM implementation to gather relevant comparisons. Please note that we have to create multiple copies of the same detector simulator to provide each tracker with the same set of detections for a fairer comparison.

Stone Soup tracker components

# Load the Kalman predictor and updater
from stonesoup.predictor.kalman import UnscentedKalmanPredictor
from stonesoup.updater.kalman import UnscentedKalmanUpdater

# Instantiate the components
predictor = UnscentedKalmanPredictor(transition_model)
updater = UnscentedKalmanUpdater(measurement_model)

# Load the Initiator, Deleter and compose the trackers
from stonesoup.deleter.time import UpdateTimeStepsDeleter
deleter = UpdateTimeStepsDeleter(3)

from stonesoup.initiator.simple import MultiMeasurementInitiator

# Load the probabilistic data associator and the tracker
from stonesoup.dataassociator.neighbour import GlobalNearestNeighbour
from stonesoup.hypothesiser.probability import PDAHypothesiser
from stonesoup.dataassociator.probability import JPDA
from stonesoup.tracker.simple import MultiTargetMixtureTracker

Design the trackers

# Start with the standard JPDA
initiator = MultiMeasurementInitiator(
    prior_state=GaussianState(np.array([0, 0, 0, 0]),
                              np.diag([5, 0.5, 5, 0.5]) ** 2,
                              timestamp=simulation_start_time),
    measurement_model=None,
    deleter=deleter,
    data_associator=GlobalNearestNeighbour(PDAHypothesiser(predictor=predictor,
                                         updater=updater,
                                         clutter_spatial_density=clutter_spatial_density,
                                         prob_detect=probability_detection)),
    updater=updater,
    min_points=2)

# Tracker
JPDA_tracker = MultiTargetMixtureTracker(
    initiator=initiator,
    deleter=deleter,
    detector=detection_sims[0],
    data_associator=JPDA(PDAHypothesiser(predictor=predictor,
                                         updater=updater,
                                         clutter_spatial_density=clutter_spatial_density,
                                         prob_detect=probability_detection)),
    updater=updater)

# Now we load the EHMJPDA, please note that the initiator is the same as the JPDA
EHM_initiator = MultiMeasurementInitiator(
    prior_state=GaussianState(np.array([0, 0, 0, 0]),
                              np.diag([5, 0.5, 5, 0.5]) ** 2,
                              timestamp=simulation_start_time),
    measurement_model=None,
    deleter=deleter,
    data_associator=GlobalNearestNeighbour(PDAHypothesiser(predictor=predictor,
                                         updater=updater,
                                         clutter_spatial_density=clutter_spatial_density,
                                         prob_detect=probability_detection,)),
    updater=updater,
    min_points=2)

# In this tracker we use the JPDA with EHM
EHM1_tracker = MultiTargetMixtureTracker(
    initiator=EHM_initiator,
    deleter=deleter,
    detector=detection_sims[1],
    data_associator=JPDAWithEHM(PDAHypothesiser(predictor=predictor,
                                         updater=updater,
                                         clutter_spatial_density=clutter_spatial_density,
                                         prob_detect=probability_detection)),
    updater=updater)

# Copy the same initiator for EHM
EHM2_initiator = deepcopy(EHM_initiator)

# This tracker uses the second implementation
# of EHM.
EHM2_tracker = MultiTargetMixtureTracker(
    initiator=EHM2_initiator,
    deleter=deleter,
    detector=detection_sims[2],
    data_associator=JPDAWithEHM2(PDAHypothesiser(predictor=predictor,
                                                updater=updater,
                                                clutter_spatial_density=clutter_spatial_density,
                                                prob_detect=probability_detection)),
    updater=updater)

3) Run the trackers to generate the tracks;

We have instantiated the three versions of the trackers, one with the brute force JPDA hypothesis management, one with the EHM implementation [1] and one with the EHM2 implementation [2]. Now, we can run the trackers and gather the final tracks as well as the detections, clutter and define a metric plotter to evaluate the track accuracy using the metric manager. As the three methods will use the same hypothesis we will obtain the same tracks, we verify such claim by comparing the OSPA metric between each hyphotesiser. To measure the significant difference in computing time we measure the time while running the three different trackers.

Stone Soup Metrics imports

# Instantiate the metrics tracker
from stonesoup.metricgenerator.basicmetrics import BasicMetrics

basic_JPDA = BasicMetrics(generator_name='basic_JPDA', tracks_key='JPDA_tracks',
                          truths_key='truths')
EHM1 = BasicMetrics(generator_name='EHM1', tracks_key='EHM1_tracks',
                    truths_key='truths')
EHM2 = BasicMetrics(generator_name='EHM2', tracks_key='EHM2_tracks',
                    truths_key='truths')

# Compare the generated tracks to verify they obtain the same
# accuracy, we consider as truths tracks the EHM tracks
from stonesoup.metricgenerator.ospametric import OSPAMetric

ospa_JPDA_EHM1 = OSPAMetric(c=40, p=1, generator_name='OSPA_JPDA-EHM1',
                            tracks_key='JPDA_tracks', truths_key='EHM1_tracks')
ospa_JPDA_EHM2 = OSPAMetric(c=40, p=1, generator_name='OSPA_JPDA-EHM2',
                           tracks_key='JPDA_tracks', truths_key='EHM2_tracks')

# Define the track data associator
from stonesoup.dataassociator.tracktotrack import TrackToTruth

associator = TrackToTruth(association_threshold=30)

# Load the plotter
from stonesoup.metricgenerator.plotter import TwoDPlotter

plot_generator_JPDA = TwoDPlotter([0, 2], [0, 2], [0, 2], uncertainty=True, tracks_key='JPDA_tracks',
                                 truths_key='truths', detections_key='detections',
                                 generator_name='JPDA_plot')
plot_generator_EHM1 = TwoDPlotter([0, 2], [0, 2], [0, 2], uncertainty=True, tracks_key='EHM1_tracks',
                                truths_key='truths', detections_key='detections',
                                generator_name='EHM1_plot')

plot_generator_EHM2 = TwoDPlotter([0, 2], [0, 2], [0, 2], uncertainty=True, tracks_key='EHM2_tracks',
                                truths_key='truths', detections_key='detections',
                                generator_name='EHM2_plot')

# Load the multi-manager
from stonesoup.metricgenerator.manager import MultiManager

# Load all the relevant components of the plots in the metric manager
metric_manager = MultiManager([basic_JPDA,
                               EHM1,
                               EHM2,
                               ospa_JPDA_EHM1,
                               ospa_JPDA_EHM2,
                               plot_generator_JPDA,
                               plot_generator_EHM1,
                               plot_generator_EHM2
                               ], associator)

Run simulation

# We  plot the various tracker results
JPDA_tracks = set()
EHM1_tracks = set()
EHM2_tracks = set()
groundtruths = set()
detections_set = set()

# We measure the computation time
start_time = perf_counter()
for time, ctracks in JPDA_tracker:
    JPDA_tracks.update(ctracks)
    detections_set.update(detection_sim.detections)
jpda_time = perf_counter() - start_time

groundtruths = groundtruth_sim.groundtruth_paths

start_time = perf_counter()
for time, etracks in EHM1_tracker:
    EHM1_tracks.update(etracks)
ehm1_time = perf_counter() - start_time

start_time = perf_counter()
for time, etracks in EHM2_tracker:
    EHM2_tracks.update(etracks)
ehm2_time = perf_counter() - start_time

# Add the various tracks to the metric manager
metric_manager.add_data({'JPDA_tracks': JPDA_tracks}, overwrite=False)
metric_manager.add_data({'truths': groundtruths,
                        'detections': detections_set}, overwrite=False)
metric_manager.add_data({'EHM1_tracks': EHM1_tracks}, overwrite=False)
metric_manager.add_data({'EHM2_tracks': EHM2_tracks}, overwrite=False)

4) Compare the trackers performances;

We have set up the trackers as well as the metric manager, to conclude this tutorial we show the results of the computing time needed for each tracker, the overall tracks generated and the differences between the tracks, if any. We start presenting the time performances of the different trackers along with the performance improvement obtained by the EHM data associators.

print('Comparisons between the trackers performances')
print(f'JPDA computing time: {jpda_time:.2f} seconds')
print(f'EHM1 computing time: {ehm1_time:.2f} seconds, {(jpda_time/ehm1_time-1)*100:.2f} % quicker than JPDA')
print(f'EHM2 computing time: {ehm2_time:.2f} seconds, {(jpda_time/ehm2_time-1)*100:.2f} % quicker than JPDA')

# Load the plotter package to plot the
# detections, tracks and detections.
from stonesoup.plotter import Plotterly

plotter = Plotterly()

plotter.plot_ground_truths(groundtruths, [0, 2])
plotter.plot_measurements(detections_set, [0, 2])
plotter.plot_tracks(JPDA_tracks, [0, 2], line= dict(color='orange'),
                    track_label='JPDA tracks')
plotter.plot_tracks(EHM1_tracks, [0, 2], line= dict(color='green', dash='dot'),
                    track_label='EHM1 tracks')
plotter.plot_tracks(EHM2_tracks, [0, 2], line= dict(color='red', dash='dot'),
                    track_label='EHM2 tracks')
plotter.fig
Comparisons between the trackers performances
JPDA computing time: 2.46 seconds
EHM1 computing time: 1.87 seconds, 31.52 % quicker than JPDA
EHM2 computing time: 1.86 seconds, 32.57 % quicker than JPDA


Show the metrics

# Now we process the metrics
metrics = metric_manager.generate_metrics()

# Load the metric plotter
from stonesoup.plotter import MetricPlotter
graph = MetricPlotter()

graph.plot_metrics(metrics, generator_names=['OSPA_JPDA-EHM1',
                                             'OSPA_JPDA-EHM2'],
                   color=['orange',  'blue'])

# update y-axis label and title, other subplots are displaying auto-generated title and labels
graph.axes[0].set(ylabel='OSPA metrics', title='OSPA distances over time between JPDA and EHMs tracks')
graph.fig
# Please note the scale of the plot
  • JPDA_plot
  • EHM1_plot
  • EHM2_plot
  • OSPA distances over time between JPDA and EHMs tracks

Conclusion

In this example we have shown how the performances of the tracker changes by employing or not an efficient management system. We measure a significant improvement (depending on the number of simulation steps, number of tracks and clutter rate) in the computation time in using EHM approaches compared to the brute force JPDA. The tracks obtained by the three trackers are perfectly aligned.

References

Total running time of the script: (0 minutes 9.447 seconds)

Gallery generated by Sphinx-Gallery