Trackers

End-to-end tracking algorithms.

End-to-end tracker implementations.

This module provides complete tracker implementations that combine filtering, data association, and track management.

Single Target Tracking

Single target tracking algorithms.

Single-target tracker implementation.

This module provides a simple single-target tracker using Kalman filtering.

class pytcl.trackers.single_target.SingleTargetTracker(state_dim=None, meas_dim=None, F=None, H=None, Q=None, R=None, gate_threshold=None, *, config=None)[source]

Bases: object

Single-target tracker using Kalman filtering.

This tracker maintains a single track and provides predict/update functionality with optional gating.

Parameters:
  • state_dim (int) – Dimension of state vector.

  • meas_dim (int) – Dimension of measurement vector.

  • F (callable or ndarray) – State transition matrix or function F(dt) -> ndarray.

  • H (ndarray) – Measurement matrix.

  • Q (callable or ndarray) – Process noise covariance or function Q(dt) -> ndarray.

  • R (ndarray) – Measurement noise covariance.

  • gate_threshold (float, optional) – Chi-squared gate threshold (default: None, no gating).

  • config (SingleTargetConfig, optional) – Typed configuration. Mutually exclusive with the individual keyword arguments above. config.F/config.Q must be set (matrix dynamics) – a config snapshotting callable dynamics cannot rebuild the tracker.

Examples

>>> import numpy as np
>>> # Constant velocity model in 2D
>>> F = lambda dt: np.array([[1, dt, 0, 0],
...                          [0, 1, 0, 0],
...                          [0, 0, 1, dt],
...                          [0, 0, 0, 1]])
>>> H = np.array([[1, 0, 0, 0],
...               [0, 0, 1, 0]])
>>> Q = lambda dt: 0.1 * np.eye(4)
>>> R = np.eye(2) * 0.5
>>> tracker = SingleTargetTracker(4, 2, F, H, Q, R)
>>> tracker.initialize(np.array([0, 1, 0, 1]), np.eye(4))
>>> predicted = tracker.predict(1.0)
>>> predicted.state
array([1., 1., 1., 1.])
>>> updated = tracker.update(np.array([1.1, 1.2]))
__init__(state_dim=None, meas_dim=None, F=None, H=None, Q=None, R=None, gate_threshold=None, *, config=None)[source]
initialize(state, covariance, time=0.0)[source]

Initialize the tracker with initial state.

Parameters:
  • state (array_like) – Initial state estimate.

  • covariance (array_like) – Initial state covariance.

  • time (float, optional) – Initial time (default: 0).

property is_initialized: bool

Check if tracker is initialized.

property state: TrackState | None

Get current track state.

predict(dt)[source]

Predict state to new time.

Parameters:

dt (float) – Time step.

Returns:

Predicted state.

Return type:

TrackState

Raises:

RuntimeError – If tracker is not initialized.

update(measurement, measurement_covariance=None)[source]

Update state with measurement.

Parameters:
  • measurement (array_like) – Measurement vector.

  • measurement_covariance (array_like, optional) –

    Covariance for this detection, shape (meas_dim, meas_dim). When omitted the tracker’s fixed R is used.

    Supply this when the measurement error varies between detections – a converted polar detection, for instance, has a Cartesian covariance that is anisotropic and grows with range, so no single R describes it. Both the gate and the Kalman gain then use the covariance that actually applies.

Returns:

  • state (TrackState) – Updated state, or – if the measurement fails the gate – the state unchanged. See Notes for telling the two apart.

  • gate_distance (float) – Squared Mahalanobis distance of the innovation, innovation @ inv(S) @ innovation. This is a distance, so smaller means a better match, and it is directly comparable to gate_threshold, which is documented as a chi-squared value. It is not a likelihood: thresholding it as though larger were better inverts the decision. MHTTracker keeps the two quantities separate and computes a real Gaussian likelihood alongside its gate distance; this method returns only the gate distance.

Raises:
  • RuntimeError – If tracker is not initialized.

  • ValueError – If measurement_covariance is not (meas_dim, meas_dim).

Return type:

tuple[TrackState, float]

Notes

When gate_threshold is set and the measurement fails the gate, the filter state is left untouched and returned as-is. The returned gate_distance is the only signal that this happened – compare it against gate_threshold if the caller needs to distinguish an applied update from a rejected measurement.

predict_measurement(measurement_covariance=None)[source]

Predict measurement and innovation covariance.

Parameters:

measurement_covariance (array_like, optional) – Covariance of the detection being considered. Omit to use the tracker’s fixed R.

Returns:

  • z_pred (ndarray) – Predicted measurement.

  • S (ndarray) – Innovation covariance.

Return type:

tuple[ndarray[tuple[Any, …], dtype[float64]], ndarray[tuple[Any, …], dtype[float64]]]

class pytcl.trackers.single_target.TrackState(state, covariance, time)[source]

Bases: NamedTuple

State of a single target track.

Variables:
  • state (ndarray) – State estimate vector.

  • covariance (ndarray) – State covariance matrix.

  • time (float) – Time of state estimate.

state: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 0

covariance: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 1

time: float

Alias for field number 2

Multi-Target Tracking

Multi-target tracking algorithms including track management.

Multi-target tracker implementation.

This module provides a multi-target tracker using GNN data association and Kalman filtering with track management (initiation, maintenance, deletion).

class pytcl.trackers.multi_target.MultiTargetTracker(state_dim=None, meas_dim=None, F=None, H=None, Q=None, R=None, gate_probability=None, confirm_hits=None, confirm_window=None, max_misses=None, init_covariance=None, *, config=None)[source]

Bases: object

Multi-target tracker with GNN data association.

This tracker maintains multiple tracks and handles: - Track initiation from unassociated measurements - Track update via GNN data association - Track confirmation (M-of-N logic) - Track deletion (miss count)

Parameters:
  • state_dim (int) – Dimension of state vector.

  • meas_dim (int) – Dimension of measurement vector.

  • F (callable or ndarray) – State transition matrix or function F(dt) -> ndarray.

  • H (ndarray) – Measurement matrix.

  • Q (callable or ndarray) – Process noise covariance or function Q(dt) -> ndarray.

  • R (ndarray) – Measurement noise covariance.

  • gate_probability (float, optional) – Gate probability for association (default: 0.99).

  • confirm_hits (int, optional) – Hits required within confirm_window to confirm a track (the M of M-of-N; default: 3). The initiating detection counts.

  • confirm_window (int, optional) – Number of most recent association outcomes examined when deciding confirmation (the N of M-of-N; default: 5).

  • max_misses (int, optional) – Consecutive misses before deletion (default: 5).

  • init_covariance (ndarray, optional) – Initial covariance for new tracks, shape (state_dim, state_dim). If None, uses 100 * I.

  • config (MultiTargetConfig, optional) – Typed configuration. Mutually exclusive with the individual keyword arguments above. config.F/config.Q must be set (matrix dynamics) – a config snapshotting callable dynamics cannot rebuild the tracker.

Examples

>>> import numpy as np
>>> # Constant velocity model
>>> F = lambda dt: np.array([[1, dt, 0, 0],
...                          [0, 1, 0, 0],
...                          [0, 0, 1, dt],
...                          [0, 0, 0, 1]])
>>> H = np.array([[1, 0, 0, 0],
...               [0, 0, 1, 0]])
>>> Q = lambda dt: 0.1 * np.eye(4)
>>> R = np.eye(2) * 0.5
>>> tracker = MultiTargetTracker(4, 2, F, H, Q, R)
>>> # Process measurements
>>> measurements = [np.array([1, 2]), np.array([5, 6])]
>>> tracks = tracker.process(measurements, dt=1.0)
__init__(state_dim=None, meas_dim=None, F=None, H=None, Q=None, R=None, gate_probability=None, confirm_hits=None, confirm_window=None, max_misses=None, init_covariance=None, *, config=None)[source]
property tracks: List[Track]

Get list of active tracks.

property confirmed_tracks: List[Track]

Get list of confirmed tracks only.

process(measurements, dt, measurement_covariances=None)[source]

Process measurements at new time step.

Parameters:
  • measurements (list of array_like) – List of measurement vectors.

  • dt (float) – Time step since last update.

  • measurement_covariances (sequence of array_like, optional) –

    Per-detection measurement covariance, one (meas_dim, meas_dim) matrix per entry in measurements. When omitted the tracker’s fixed R is used for every detection.

    Supply this when the measurement error is not the same for every detection. The usual case is a converted polar detection: its Cartesian covariance is J R_polar J^T, which is anisotropic and grows with range, so no single R describes it. Forcing one makes the gate either too tight at long range – true detections fall outside it and the tracker spawns duplicate tracks – or too loose at short range, which admits clutter and inflates the covariance.

Returns:

Active tracks after update.

Return type:

list of Track

Raises:

ValueError – If measurement_covariances is given and its length does not match measurements, or a matrix is not (meas_dim, meas_dim).

class pytcl.trackers.multi_target.Track(id, state, covariance, status, hits, misses, time)[source]

Bases: NamedTuple

Multi-target track.

Variables:
  • id (int) – Unique track identifier.

  • state (ndarray) – State estimate vector.

  • covariance (ndarray) – State covariance matrix.

  • status (TrackStatus) – Track status.

  • hits (int) – Number of measurement updates.

  • misses (int) – Number of consecutive missed detections.

  • time (float) – Time of last update.

id: int

Alias for field number 0

state: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 1

covariance: ndarray[tuple[Any, ...], dtype[float64]]

Alias for field number 2

status: TrackStatus

Alias for field number 3

hits: int

Alias for field number 4

misses: int

Alias for field number 5

time: float

Alias for field number 6

class pytcl.trackers.multi_target.TrackStatus(value)[source]

Bases: Enum

Track status enumeration.

TENTATIVE = 'tentative'
CONFIRMED = 'confirmed'
DELETED = 'deleted'

Multiple Hypothesis Tracking (MHT)

Hypothesis-oriented MHT implementation.

Multiple Hypothesis Tracking (MHT) implementation.

MHT maintains multiple hypotheses about measurement-to-track associations, deferring hard decisions until more information is available. This allows the tracker to recover from association errors.

This implementation uses track-oriented MHT with N-scan pruning.

References

  • S. Blackman and R. Popoli, “Design and Analysis of Modern Tracking Systems,” Artech House, 1999.

  • D. Reid, “An Algorithm for Tracking Multiple Targets,” IEEE Trans. Automatic Control, 1979.

class pytcl.trackers.mht.MHTConfig(n_scan=3, max_hypotheses=100, detection_prob=0.9, clutter_density=1e-06, gate_probability=0.99, confirm_threshold=3, delete_threshold=5, min_hypothesis_prob=1e-06, new_track_weight=0.1)[source]

Bases: Struct

Configuration for MHT tracker.

Variables:
  • n_scan (int) – Number of scans for N-scan pruning. Default 3.

  • max_hypotheses (int) – Maximum number of hypotheses to maintain. Default 100.

  • detection_prob (float) – Probability of detection (Pd). Default 0.9.

  • clutter_density (float) – Spatial density of false alarms. Default 1e-6.

  • gate_probability (float) – Gating probability for chi-squared test. Default 0.99.

  • confirm_threshold (int) – Number of hits to confirm a track. Default 3.

  • delete_threshold (int) – Number of consecutive misses to delete a track. Default 5.

  • min_hypothesis_prob (float) – Minimum hypothesis probability. Default 1e-6.

  • new_track_weight (float) – Prior weight for new track hypothesis. Default 0.1.

n_scan: int
max_hypotheses: int
detection_prob: float
clutter_density: float
gate_probability: float
confirm_threshold: int
delete_threshold: int
min_hypothesis_prob: float
new_track_weight: float
class pytcl.trackers.mht.MHTResult(confirmed_tracks, tentative_tracks, all_tracks, n_hypotheses, best_hypothesis_prob)[source]

Bases: NamedTuple

Result of MHT processing step.

Variables:
  • confirmed_tracks (list of MHTTrack) – Tracks that are confirmed.

  • tentative_tracks (list of MHTTrack) – Tracks that are tentative.

  • all_tracks (list of MHTTrack) – All active tracks from best hypothesis.

  • n_hypotheses (int) – Number of active hypotheses.

  • best_hypothesis_prob (float) – Probability of the best hypothesis.

confirmed_tracks: List[MHTTrack]

Alias for field number 0

tentative_tracks: List[MHTTrack]

Alias for field number 1

all_tracks: List[MHTTrack]

Alias for field number 2

n_hypotheses: int

Alias for field number 3

best_hypothesis_prob: float

Alias for field number 4

class pytcl.trackers.mht.MHTTracker(state_dim, meas_dim, F, H, Q, R, config=None, init_covariance=None)[source]

Bases: object

Multiple Hypothesis Tracking (MHT) tracker.

Maintains multiple hypotheses about measurement-to-track associations, with N-scan pruning for complexity control.

Parameters:
  • state_dim (int) – Dimension of state vector.

  • meas_dim (int) – Dimension of measurement vector.

  • F (callable or ndarray) – State transition matrix or function F(dt) -> ndarray.

  • H (ndarray) – Measurement matrix.

  • Q (callable or ndarray) – Process noise covariance or function Q(dt) -> ndarray.

  • R (ndarray) – Measurement noise covariance.

  • config (MHTConfig, optional) – Tracker configuration. Uses defaults if not provided.

  • init_covariance (ndarray, optional) – Initial covariance for new tracks.

Examples

>>> import numpy as np
>>> # Constant velocity model
>>> F = lambda dt: np.array([[1, dt, 0, 0],
...                          [0, 1, 0, 0],
...                          [0, 0, 1, dt],
...                          [0, 0, 0, 1]])
>>> H = np.array([[1, 0, 0, 0],
...               [0, 0, 1, 0]])
>>> Q = lambda dt: 0.1 * np.eye(4)
>>> R = np.eye(2) * 0.5
>>> tracker = MHTTracker(4, 2, F, H, Q, R)
>>> # Process measurements
>>> measurements = [np.array([1, 2]), np.array([5, 6])]
>>> result = tracker.process(measurements, dt=1.0)
__init__(state_dim, meas_dim, F, H, Q, R, config=None, init_covariance=None)[source]
process(measurements, dt)[source]

Process measurements at new time step.

Parameters:
  • measurements (list of array_like) – List of measurement vectors.

  • dt (float) – Time step since last update.

Returns:

result – Tracking result with confirmed and tentative tracks.

Return type:

MHTResult

property tracks: List[MHTTrack]

Get all tracks from best hypothesis.

property confirmed_tracks: List[MHTTrack]

Get confirmed tracks from best hypothesis.

property n_hypotheses: int

Number of active hypotheses.

Hypothesis Management

Track hypothesis creation, scoring, and pruning.

Hypothesis management for Multiple Hypothesis Tracking (MHT).

This module provides data structures and algorithms for managing hypothesis trees in track-oriented MHT implementations.

References

  • S. Blackman and R. Popoli, “Design and Analysis of Modern Tracking Systems,” Artech House, 1999.

  • D. Reid, “An Algorithm for Tracking Multiple Targets,” IEEE Trans. Automatic Control, 1979.

class pytcl.trackers.hypothesis.MHTTrackStatus(value)[source]

Bases: Enum

Track status in MHT.

TENTATIVE = 'tentative'
CONFIRMED = 'confirmed'
DELETED = 'deleted'
class pytcl.trackers.hypothesis.MHTTrack(id, state, covariance, score, status, history, parent_id, scan_created, n_hits, n_misses)[source]

Bases: NamedTuple

Track state within MHT.

Variables:
  • id (int) – Unique track identifier.

  • state (ndarray) – State estimate vector.

  • covariance (ndarray) – State covariance matrix.

  • score (float) – Log-likelihood ratio score.

  • status (MHTTrackStatus) – Track status.

  • history (list of int) – Measurement indices associated with this track branch. -1 indicates a missed detection.

  • parent_id (int) – ID of parent track (-1 for root tracks).

  • scan_created (int) – Scan number when this track branch was created.

  • n_hits (int) – Number of measurement updates.

  • n_misses (int) – Number of consecutive misses.

id: int

Alias for field number 0

state: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

covariance: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

score: float

Alias for field number 3

status: MHTTrackStatus

Alias for field number 4

history: List[int]

Alias for field number 5

parent_id: int

Alias for field number 6

scan_created: int

Alias for field number 7

n_hits: int

Alias for field number 8

n_misses: int

Alias for field number 9

class pytcl.trackers.hypothesis.Hypothesis(id, probability, track_ids, scan_created, parent_id)[source]

Bases: NamedTuple

A global hypothesis in MHT.

A hypothesis represents a consistent assignment of measurements to tracks across multiple scans. Each hypothesis maintains a set of track branches that are mutually compatible.

Variables:
  • id (int) – Unique hypothesis identifier.

  • probability (float) – Posterior probability of this hypothesis.

  • track_ids (list of int) – IDs of tracks included in this hypothesis.

  • scan_created (int) – Scan number when this hypothesis was created.

  • parent_id (int) – ID of parent hypothesis (-1 for initial hypothesis).

id: int

Alias for field number 0

probability: float

Alias for field number 1

track_ids: List[int]

Alias for field number 2

scan_created: int

Alias for field number 3

parent_id: int

Alias for field number 4

pytcl.trackers.hypothesis.generate_joint_associations(gated, n_tracks, n_meas)[source]

Generate all valid joint measurement-to-track associations.

A valid association satisfies: - Each measurement is assigned to at most one track - Each track is assigned to at most one measurement - Only gated track-measurement pairs are considered

Parameters:
  • gated (ndarray) – Boolean gating matrix, shape (n_tracks, n_meas). gated[i, j] = True if track i can be associated with measurement j.

  • n_tracks (int) – Number of tracks.

  • n_meas (int) – Number of measurements.

Returns:

associations – List of valid associations. Each dict maps track_id to meas_idx. meas_idx = -1 indicates missed detection.

Return type:

list of dict

Examples

>>> gated = np.array([[True, True], [True, True]])
>>> associations = generate_joint_associations(gated, 2, 2)
>>> len(associations)  # All valid 2-track, 2-measurement associations
7
pytcl.trackers.hypothesis.compute_association_likelihood(association, likelihood_matrix, detection_prob, clutter_density, n_meas)[source]

Compute likelihood of a joint association.

Parameters:
  • association (dict) – Mapping from track_id to measurement_idx (-1 for miss).

  • likelihood_matrix (ndarray) – Likelihood values, shape (n_tracks, n_meas).

  • detection_prob (float) – Probability of detection.

  • clutter_density (float) – Spatial density of clutter (false alarms).

  • n_meas (int) – Total number of measurements.

Returns:

likelihood – Joint likelihood of the association.

Return type:

float

Notes

Unassigned measurements contribute a plain clutter term, clutter_density ** n_clutter. This is a different formula from pytcl.trackers.mht.MHTTracker._compute_association_likelihood(), whose unassigned-measurement term folds in new_track_weight as well ((clutter_density + new_track_weight) ** n_unassigned), since MHT must also account for the possibility that an unassigned measurement starts a new track rather than being clutter. MHTTracker uses its own internal method, not this one.

Examples

>>> import numpy as np
>>> # 2 tracks, 2 measurements
>>> likelihood_matrix = np.array([[0.9, 0.1],
...                                [0.1, 0.8]])
>>> # Association: track 0 -> meas 0, track 1 -> meas 1
>>> association = {0: 0, 1: 1}
>>> lik = compute_association_likelihood(
...     association, likelihood_matrix,
...     detection_prob=0.9, clutter_density=1e-6, n_meas=2
... )
>>> lik > 0
True
>>> # Association with missed detection
>>> assoc_miss = {0: 0, 1: -1}  # track 1 misses
>>> lik_miss = compute_association_likelihood(
...     assoc_miss, likelihood_matrix,
...     detection_prob=0.9, clutter_density=1e-6, n_meas=2
... )
>>> lik > lik_miss  # Full detection more likely
True
pytcl.trackers.hypothesis.n_scan_prune(hypotheses, tracks, n_scan, current_scan)[source]

N-scan pruning of hypotheses.

Removes hypotheses that diverged from the most likely hypothesis more than n_scan scans ago. This implements “deferred decision” pruning where associations older than N scans are committed to.

Parameters:
  • hypotheses (list of Hypothesis) – Current hypotheses.

  • tracks (dict) – Mapping from track_id to MHTTrack.

  • n_scan (int) – Number of scans to look back.

  • current_scan (int) – Current scan number.

Returns:

  • pruned_hypotheses (list of Hypothesis) – Hypotheses surviving pruning.

  • committed_track_ids (set) – Track IDs that are now committed (survived N-scan).

Return type:

Tuple[List[Hypothesis], Set[int]]

Examples

>>> import numpy as np
>>> from pytcl.trackers.hypothesis import (
...     Hypothesis, MHTTrack, MHTTrackStatus, n_scan_prune
... )
>>> # Two hypotheses, tracks with different creation scans
>>> track1 = MHTTrack(id=0, state=np.zeros(2), covariance=np.eye(2),
...                   score=1.0, status=MHTTrackStatus.CONFIRMED,
...                   history=[0], parent_id=-1, scan_created=0,
...                   n_hits=3, n_misses=0)
>>> track2 = MHTTrack(id=1, state=np.zeros(2), covariance=np.eye(2),
...                   score=0.5, status=MHTTrackStatus.TENTATIVE,
...                   history=[1], parent_id=-1, scan_created=2,
...                   n_hits=1, n_misses=0)
>>> tracks = {0: track1, 1: track2}
>>> hyp1 = Hypothesis(id=0, probability=0.8, track_ids=[0],
...                   scan_created=0, parent_id=-1)
>>> hyp2 = Hypothesis(id=1, probability=0.2, track_ids=[1],
...                   scan_created=2, parent_id=-1)
>>> pruned, committed = n_scan_prune([hyp1, hyp2], tracks, n_scan=2,
...                                   current_scan=3)
>>> len(pruned) >= 1
True

Notes

This implementation applies a single-MAP-hypothesis rule, not agreement across all high-probability hypotheses:

1. Take the single highest-probability (MAP) hypothesis and,
   for each of its track branches, walk the ``parent_id`` chain
   back to the ancestor branch whose association decision was
   made at or before scan (current_scan - n_scan). That ancestor
   set is what the MAP hypothesis has committed to.
2. Keep a hypothesis only if its own ancestor set at that scan
   is identical to the MAP hypothesis's set. Track families born
   after the cutoff have no ancestor there and are excluded from
   the comparison on both sides.

Hypotheses that agree with each other but disagree with the MAP hypothesis are pruned along with genuinely divergent ones – the rule is “match the MAP hypothesis or be removed,” not a vote across all high-probability hypotheses.

pytcl.trackers.hypothesis.prune_hypotheses_by_probability(hypotheses, max_hypotheses, min_probability=1e-06)[source]

Prune hypotheses by probability threshold and count limit.

Parameters:
  • hypotheses (list of Hypothesis) – Current hypotheses.

  • max_hypotheses (int) – Maximum number of hypotheses to retain.

  • min_probability (float) – Minimum probability threshold.

Returns:

pruned – Pruned and renormalized hypotheses.

Return type:

list of Hypothesis

Examples

>>> from pytcl.trackers.hypothesis import Hypothesis, prune_hypotheses_by_probability
>>> # 5 hypotheses with varying probabilities
>>> hyps = [
...     Hypothesis(id=0, probability=0.5, track_ids=[0], scan_created=0, parent_id=-1),
...     Hypothesis(id=1, probability=0.3, track_ids=[1], scan_created=0, parent_id=-1),
...     Hypothesis(id=2, probability=0.1, track_ids=[2], scan_created=0, parent_id=-1),
...     Hypothesis(id=3, probability=0.05, track_ids=[3], scan_created=0, parent_id=-1),
...     Hypothesis(id=4, probability=1e-8, track_ids=[4], scan_created=0, parent_id=-1),
... ]
>>> pruned = prune_hypotheses_by_probability(hyps, max_hypotheses=3)
>>> len(pruned)  # Only top 3 kept
3
>>> sum(h.probability for h in pruned)  # Renormalized to 1
1.0
class pytcl.trackers.hypothesis.HypothesisTree(max_hypotheses=100, n_scan=3, min_probability=1e-06)[source]

Bases: object

Manages hypothesis tree for MHT.

The hypothesis tree represents all possible interpretations of measurement-to-track associations across multiple scans.

Parameters:
  • max_hypotheses (int) – Maximum number of hypotheses to maintain.

  • n_scan (int) – Number of scans for N-scan pruning.

  • min_probability (float) – Minimum hypothesis probability threshold.

Variables:
  • hypotheses (list of Hypothesis) – Current set of hypotheses.

  • tracks (dict) – Mapping from track_id to MHTTrack.

  • current_scan (int) – Current scan number.

__init__(max_hypotheses=100, n_scan=3, min_probability=1e-06)[source]
hypotheses: List[Hypothesis]
tracks: Dict[int, MHTTrack]
initialize(initial_tracks=None)[source]

Initialize the hypothesis tree.

Parameters:

initial_tracks (list of MHTTrack, optional) – Initial tracks to include.

add_track(track)[source]

Add a new track to the tree.

Parameters:

track (MHTTrack) – Track to add.

Returns:

track_id – ID assigned to the track.

Return type:

int

prune()[source]

Apply all pruning strategies.

get_best_hypothesis()[source]

Get the most probable hypothesis.

get_best_tracks()[source]

Get tracks from the best hypothesis.

get_confirmed_tracks()[source]

Get confirmed tracks from the best hypothesis.

Bernoulli Filter

Joint target existence and state estimation for principled track initiation and termination.

Bernoulli filter for joint target existence and state estimation.

The Bernoulli filter is the exact Bayes filter for a target that may or may not exist: the posterior is a Bernoulli random finite set with existence probability r and, conditioned on existence, a spatial density. This implementation uses a single-Gaussian spatial density with moment matching of the prediction/update mixtures, the common Kalman (linear-Gaussian) realization of the filter.

It is the principled replacement for ad-hoc M-of-N track initiation and termination logic: r rises as detections consistent with the dynamics accumulate and decays through missed detections.

References

  • B. Ristic, B.-T. Vo, B.-N. Vo, and A. Farina, “A Tutorial on Bernoulli Filters: Theory, Implementation and Applications,” IEEE Transactions on Signal Processing, 61(13), 2013.

class pytcl.trackers.bernoulli.BernoulliConfig(birth_prob=0.01, survival_prob=0.99, detection_prob=0.9, clutter_intensity=0.0001)[source]

Bases: Struct

Configuration for the Bernoulli filter.

Variables:
  • birth_prob (float) – Probability p_b that a target is born between scans, given it does not currently exist. Default 0.01.

  • survival_prob (float) – Probability p_s that an existing target survives to the next scan. Default 0.99.

  • detection_prob (float) – Probability of detection Pd. Default 0.9.

  • clutter_intensity (float) – Poisson clutter intensity kappa (expected clutter measurements per unit measurement volume), assumed uniform. Must be positive: the update equations divide detection densities by it. A truly clutter-free sensor can pass a small value. Default 1e-4.

birth_prob: float
survival_prob: float
detection_prob: float
clutter_intensity: float
class pytcl.trackers.bernoulli.BernoulliState(r, x, P)[source]

Bases: NamedTuple

Bernoulli filter posterior.

Variables:
  • r (float) – Probability that the target exists.

  • x (ndarray) – State mean, conditioned on existence.

  • P (ndarray) – State covariance, conditioned on existence.

r: float

Alias for field number 0

x: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 1

P: ndarray[tuple[Any, ...], dtype[floating]]

Alias for field number 2

pytcl.trackers.bernoulli.bernoulli_predict(state, F, Q, birth_mean, birth_cov, config=None)[source]

Bernoulli filter prediction step.

The predicted existence probability combines birth and survival,

\[r_{k|k-1} = p_b (1 - r_{k-1}) + p_s r_{k-1},\]

and the predicted spatial density is the corresponding mixture of the birth density and the survived (Kalman-predicted) density, moment-matched to a single Gaussian (Ristic et al. 2013, Sec. V).

Parameters:
  • state (BernoulliState) – Current posterior.

  • F (array_like) – State transition matrix.

  • Q (array_like) – Process noise covariance.

  • birth_mean (array_like) – Mean of the birth density.

  • birth_cov (array_like) – Covariance of the birth density.

  • config (BernoulliConfig, optional) – Filter parameters. Uses defaults if not provided.

Returns:

predicted – Predicted Bernoulli state.

Return type:

BernoulliState

Examples

>>> import numpy as np
>>> from pytcl.trackers.bernoulli import (
...     BernoulliConfig, BernoulliState, bernoulli_predict
... )
>>> config = BernoulliConfig(birth_prob=0.1, survival_prob=0.9)
>>> state = BernoulliState(r=0.5, x=np.zeros(2), P=np.eye(2))
>>> F = np.eye(2)
>>> Q = 0.01 * np.eye(2)
>>> pred = bernoulli_predict(state, F, Q, np.zeros(2), 10 * np.eye(2),
...                          config)
>>> round(pred.r, 3)  # 0.1 * 0.5 + 0.9 * 0.5
0.5
pytcl.trackers.bernoulli.bernoulli_update(state, measurements, H, R, config=None)[source]

Bernoulli filter update step.

With Poisson clutter of uniform intensity kappa and detection probability Pd, the existence update is (Ristic et al. 2013, eq. (87))

\[\delta_k = P_d \Bigl(1 - \sum_{z} \frac{q(z)}{\kappa}\Bigr), \qquad r_k = r_{k|k-1} \frac{1 - \delta_k}{1 - r_{k|k-1} \delta_k},\]

where \(q(z) = \mathcal{N}(z; H x_{k|k-1}, S)\) is the predicted measurement density. The spatial density becomes a mixture of the missed-detection term (weight \(1 - P_d\)) and one Kalman-updated term per measurement (weight \(P_d\, q(z)/\kappa\)), normalized and moment-matched to a single Gaussian.

Parameters:
  • state (BernoulliState) – Predicted Bernoulli state.

  • measurements (list of array_like) – Measurements received this scan (possibly empty).

  • H (array_like) – Measurement matrix.

  • R (array_like) – Measurement noise covariance.

  • config (BernoulliConfig, optional) – Filter parameters. Uses defaults if not provided.

Returns:

updated – Updated Bernoulli state.

Return type:

BernoulliState

Raises:

ValueError – If config.clutter_intensity is not positive: the update divides detection densities by it.

Examples

>>> import numpy as np
>>> from pytcl.trackers.bernoulli import (
...     BernoulliConfig, BernoulliState, bernoulli_update
... )
>>> config = BernoulliConfig(detection_prob=0.9, clutter_intensity=1e-3)
>>> state = BernoulliState(r=0.5, x=np.zeros(2), P=np.eye(2))
>>> H = np.eye(2)
>>> R = 0.1 * np.eye(2)
>>> upd = bernoulli_update(state, [np.array([0.1, -0.1])], H, R, config)
>>> upd.r > state.r  # an on-prediction detection raises existence
True
>>> miss = bernoulli_update(state, [], H, R, config)
>>> miss.r < state.r  # no detection lowers existence
True