Tracking Containers
This example demonstrates track and measurement container data structures.
Overview
Efficient tracking systems require organized data structures:
TrackList: Collection of tracks with spatial queries
MeasurementSet: Organized measurement storage
Track state: Position, velocity, covariance, metadata
Key Concepts
Track ID management: Unique identifiers for each track
Temporal indexing: Accessing data by time step
Spatial queries: Finding tracks in a region
Track history: Storing past states for smoothing
Spatial Indexing: KD-trees enable efficient nearest-neighbor queries for track-to-measurement association.
Range Queries: R-trees support efficient rectangular range queries for gating operations.
Code Highlights
The example demonstrates:
Creating and populating TrackList containers
Adding tracks with state and covariance
Querying tracks by ID, time, or spatial region
Iterating over tracks for batch processing
Source Code
1"""
2Tracking Containers Example
3===========================
4
5This example demonstrates the tracking container classes in PyTCL:
6- TrackList: Collection of tracks with filtering and batch operations
7- MeasurementSet: Time-indexed measurements with spatial queries
8- ClusterSet: Track clustering for formation detection
9
10These containers provide efficient data management for multi-target tracking
11applications with immutable design patterns and lazy spatial indexing.
12"""
13
14import os
15from pathlib import Path
16
17import numpy as np
18import plotly.graph_objects as go
19from plotly.subplots import make_subplots
20
21from pytcl.containers import (
22 ClusterSet,
23 MeasurementSet,
24 TrackList,
25)
26from pytcl.containers.cluster_set import cluster_tracks_dbscan, cluster_tracks_kmeans
27from pytcl.containers.measurement_set import Measurement
28from pytcl.containers.track_list import Track, TrackStatus
29
30SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
31OUTPUT_DIR = Path(__file__).resolve().parent / "output"
32
33
34def create_sample_tracks(n_tracks: int = 10, seed: int = 42) -> TrackList:
35 """Create sample tracks for demonstration."""
36 rng = np.random.default_rng(seed)
37
38 tracks = []
39 for i in range(n_tracks):
40 # State: [x, vx, y, vy] - 2D position and velocity
41 x = rng.uniform(-100, 100)
42 y = rng.uniform(-100, 100)
43 vx = rng.uniform(-5, 5)
44 vy = rng.uniform(-5, 5)
45 state = np.array([x, vx, y, vy])
46
47 # Covariance matrix
48 pos_var = rng.uniform(1, 5)
49 vel_var = rng.uniform(0.1, 0.5)
50 P = np.diag([pos_var, vel_var, pos_var, vel_var])
51
52 # Random status based on hits
53 hits = rng.integers(1, 20)
54 misses = rng.integers(0, 5)
55 if hits >= 5:
56 status = TrackStatus.CONFIRMED
57 elif misses >= 3:
58 status = TrackStatus.DELETED
59 else:
60 status = TrackStatus.TENTATIVE
61
62 track = Track(
63 id=i,
64 state=state,
65 covariance=P,
66 status=status,
67 hits=hits,
68 misses=misses,
69 time=10.0 + rng.uniform(0, 5),
70 )
71 tracks.append(track)
72
73 return TrackList(tracks)
74
75
76def create_sample_measurements(n_times: int = 5, n_per_time: int = 8, seed: int = 42):
77 """Create sample measurements across multiple time steps."""
78 rng = np.random.default_rng(seed)
79
80 measurements = []
81 meas_id = 0
82 for t in range(n_times):
83 time = float(t)
84 for _ in range(n_per_time):
85 # 2D position measurement
86 value = rng.uniform(-50, 50, size=2)
87 covariance = np.eye(2) * rng.uniform(0.5, 2.0)
88 sensor_id = rng.integers(0, 3) # 3 sensors
89
90 meas = Measurement(
91 value=value,
92 time=time,
93 covariance=covariance,
94 sensor_id=sensor_id,
95 id=meas_id,
96 )
97 measurements.append(meas)
98 meas_id += 1
99
100 return MeasurementSet(measurements)
101
102
103def demo_track_list():
104 """Demonstrate TrackList container operations."""
105 print("=" * 70)
106 print("TrackList Container Demo")
107 print("=" * 70)
108
109 # Create sample tracks
110 tracks = create_sample_tracks(n_tracks=15)
111 print(f"\nCreated TrackList with {len(tracks)} tracks")
112
113 # Get statistics
114 stats = tracks.stats()
115 print("\nTrack Statistics:")
116 print(f" Total tracks: {stats.n_tracks}")
117 print(f" Confirmed: {stats.n_confirmed}")
118 print(f" Tentative: {stats.n_tentative}")
119 print(f" Deleted: {stats.n_deleted}")
120 print(f" Mean hits: {stats.mean_hits:.1f}")
121 print(f" Mean misses: {stats.mean_misses:.1f}")
122
123 # Filter by status
124 confirmed = tracks.filter_by_status(TrackStatus.CONFIRMED)
125 tentative = tracks.filter_by_status(TrackStatus.TENTATIVE)
126 print("\nFiltered by status:")
127 print(f" Confirmed tracks: {len(confirmed)}")
128 print(f" Tentative tracks: {len(tentative)}")
129
130 # Shortcut properties
131 print(f" Using .confirmed property: {len(tracks.confirmed)}")
132 print(f" Using .tentative property: {len(tracks.tentative)}")
133
134 # Filter by region (tracks near origin)
135 center = np.array([0.0, 0.0])
136 nearby = tracks.filter_by_region(center, radius=50.0, state_indices=(0, 2))
137 print(f"\nTracks within 50 units of origin: {len(nearby)}")
138
139 # Filter by time
140 recent = tracks.filter_by_time(min_time=12.0)
141 print(f"Tracks updated after t=12.0: {len(recent)}")
142
143 # Custom predicate filter
144 high_confidence = tracks.filter_by_predicate(lambda t: t.hits >= 10)
145 print(f"Tracks with 10+ hits: {len(high_confidence)}")
146
147 # Batch data extraction
148 if len(confirmed) > 0:
149 states = confirmed.states()
150 positions = confirmed.positions(indices=(0, 2))
151 print("\nBatch extraction from confirmed tracks:")
152 print(f" States shape: {states.shape}")
153 print(f" Positions shape: {positions.shape}")
154
155 # Access by ID
156 track_ids = tracks.track_ids
157 if track_ids:
158 track = tracks.get_by_id(track_ids[0])
159 print(f"\nTrack {track.id}:")
160 print(f" Position: ({track.state[0]:.1f}, {track.state[2]:.1f})")
161 print(f" Velocity: ({track.state[1]:.1f}, {track.state[3]:.1f})")
162 print(f" Status: {track.status.name}")
163
164 # Immutable operations
165 new_track = Track(
166 id=100,
167 state=np.array([0, 0, 0, 0]),
168 covariance=np.eye(4),
169 status=TrackStatus.TENTATIVE,
170 hits=1,
171 misses=0,
172 time=15.0,
173 )
174 tracks_with_new = tracks.add(new_track)
175 print("\nAfter adding track:")
176 print(f" Original TrackList: {len(tracks)} tracks")
177 print(f" New TrackList: {len(tracks_with_new)} tracks")
178
179 # Merge two track lists
180 merged = confirmed.merge(tentative)
181 print(f"\nMerged confirmed + tentative: {len(merged)} tracks")
182
183
184def demo_measurement_set():
185 """Demonstrate MeasurementSet container operations."""
186 print("\n" + "=" * 70)
187 print("MeasurementSet Container Demo")
188 print("=" * 70)
189
190 # Create sample measurements
191 meas_set = create_sample_measurements(n_times=5, n_per_time=8)
192 print(f"\nCreated MeasurementSet with {len(meas_set)} measurements")
193
194 # Time properties
195 times = meas_set.times
196 time_range = meas_set.time_range
197 print("\nTime information:")
198 print(f" Unique times: {times}")
199 print(f" Time range: {time_range}")
200
201 # Query by time
202 at_t2 = meas_set.at_time(2.0)
203 print(f"\nMeasurements at t=2.0: {len(at_t2)}")
204
205 # Query time window
206 window = meas_set.in_time_window(1.0, 3.0)
207 print(f"Measurements in window [1.0, 3.0]: {len(window)}")
208
209 # Query by sensor
210 sensors = meas_set.sensors
211 print(f"\nSensors: {sensors}")
212 for sensor_id in sensors:
213 sensor_meas = meas_set.by_sensor(sensor_id)
214 print(f" Sensor {sensor_id}: {len(sensor_meas)} measurements")
215
216 # Spatial queries
217 center = np.array([0.0, 0.0])
218 nearby = meas_set.in_region(center, radius=25.0)
219 print(f"\nMeasurements within 25 units of origin: {len(nearby)}")
220
221 # K-nearest neighbors
222 query_point = np.array([10.0, 10.0])
223 nearest = meas_set.nearest_to(query_point, k=3)
224 print("\n3 nearest measurements to (10, 10):")
225 for meas in nearest.measurements:
226 dist = np.linalg.norm(meas.value - query_point)
227 print(f" ID {meas.id}: value={meas.value}, distance={dist:.2f}")
228
229 # Batch extraction
230 values = meas_set.values()
231 print("\nBatch extraction:")
232 print(f" All values shape: {values.shape}")
233
234 values_at_t1 = meas_set.values_at_time(1.0)
235 print(f" Values at t=1.0 shape: {values_at_t1.shape}")
236
237 # Create from arrays
238 new_values = np.random.randn(5, 2) * 10
239 new_times = np.array([10.0, 10.0, 10.1, 10.1, 10.2])
240 meas_from_arrays = MeasurementSet.from_arrays(new_values, new_times)
241 print(f"\nCreated from arrays: {len(meas_from_arrays)} measurements")
242
243
244def demo_cluster_set():
245 """Demonstrate ClusterSet container operations."""
246 print("\n" + "=" * 70)
247 print("ClusterSet Container Demo")
248 print("=" * 70)
249
250 # Create tracks with some spatial clustering
251 rng = np.random.default_rng(42)
252 tracks = []
253
254 # Cluster 1: tracks near (50, 50)
255 for i in range(5):
256 state = np.array(
257 [
258 50 + rng.normal(0, 3), # x
259 2 + rng.normal(0, 0.5), # vx
260 50 + rng.normal(0, 3), # y
261 1 + rng.normal(0, 0.5), # vy
262 ]
263 )
264 tracks.append(
265 Track(
266 id=i,
267 state=state,
268 covariance=np.eye(4),
269 status=TrackStatus.CONFIRMED,
270 hits=10,
271 misses=0,
272 time=0.0,
273 )
274 )
275
276 # Cluster 2: tracks near (-30, -30)
277 for i in range(4):
278 state = np.array(
279 [
280 -30 + rng.normal(0, 3),
281 -1 + rng.normal(0, 0.5),
282 -30 + rng.normal(0, 3),
283 2 + rng.normal(0, 0.5),
284 ]
285 )
286 tracks.append(
287 Track(
288 id=5 + i,
289 state=state,
290 covariance=np.eye(4),
291 status=TrackStatus.CONFIRMED,
292 hits=8,
293 misses=1,
294 time=0.0,
295 )
296 )
297
298 # Isolated tracks (noise)
299 for i in range(3):
300 state = np.array(
301 [
302 rng.uniform(-100, 100),
303 rng.uniform(-3, 3),
304 rng.uniform(-100, 100),
305 rng.uniform(-3, 3),
306 ]
307 )
308 tracks.append(
309 Track(
310 id=9 + i,
311 state=state,
312 covariance=np.eye(4),
313 status=TrackStatus.CONFIRMED,
314 hits=5,
315 misses=2,
316 time=0.0,
317 )
318 )
319
320 track_list = TrackList(tracks)
321 print(f"\nCreated {len(track_list)} tracks with 2 clusters + noise")
322
323 # DBSCAN clustering
324 print("\n--- DBSCAN Clustering ---")
325 clusters_dbscan = cluster_tracks_dbscan(
326 track_list,
327 eps=10.0, # Max distance between neighbors
328 min_samples=3, # Minimum cluster size
329 state_indices=(0, 2), # Use x, y positions
330 )
331 print(f"Found {len(clusters_dbscan)} clusters")
332
333 for cluster in clusters_dbscan:
334 print(f"\n Cluster {cluster.id}:")
335 print(f" Track IDs: {cluster.track_ids}")
336 print(f" Centroid: ({cluster.centroid[0]:.1f}, {cluster.centroid[1]:.1f})")
337 print(f" Covariance diagonal: {np.diag(cluster.covariance)}")
338
339 # Cluster statistics
340 print("\n--- Cluster Statistics ---")
341 all_stats = clusters_dbscan.all_stats(
342 tracks=track_list,
343 state_indices=(0, 2),
344 velocity_indices=(1, 3),
345 )
346 for cluster_id, stats in all_stats.items():
347 print(f"\n Cluster {cluster_id}:")
348 print(f" Tracks: {stats.n_tracks}")
349 print(f" Mean separation: {stats.mean_separation:.2f}")
350 print(f" Max separation: {stats.max_separation:.2f}")
351 print(f" Velocity coherence: {stats.velocity_coherence:.2f}")
352
353 # K-means clustering
354 print("\n--- K-Means Clustering ---")
355 clusters_kmeans = cluster_tracks_kmeans(
356 track_list,
357 n_clusters=3,
358 state_indices=(0, 2),
359 rng=np.random.default_rng(42),
360 )
361 print(f"Created {len(clusters_kmeans)} clusters")
362
363 for cluster in clusters_kmeans:
364 print(
365 f" Cluster {cluster.id}: {len(cluster.track_ids)} tracks at "
366 f"({cluster.centroid[0]:.1f}, {cluster.centroid[1]:.1f})"
367 )
368
369 # Using ClusterSet.from_tracks factory
370 print("\n--- Factory Method ---")
371 clusters = ClusterSet.from_tracks(
372 track_list,
373 method="dbscan",
374 eps=10.0,
375 min_samples=2,
376 )
377 print(f"Created ClusterSet with {len(clusters)} clusters")
378
379 # Spatial query on clusters
380 center = np.array([50.0, 50.0])
381 nearby_clusters = clusters.clusters_in_region(center, radius=30.0)
382 print(f"\nClusters within 30 units of (50, 50): {len(nearby_clusters)}")
383
384 # Track to cluster lookup
385 if len(clusters) > 0:
386 track_id = 0
387 cluster = clusters.get_cluster_for_track(track_id)
388 if cluster:
389 print(f"Track {track_id} belongs to cluster {cluster.id}")
390
391 # Cluster manipulation (immutable)
392 if len(clusters) >= 2:
393 cluster_ids = clusters.cluster_ids
394 merged = clusters.merge_clusters(cluster_ids[0], cluster_ids[1])
395 print(f"\nAfter merging clusters {cluster_ids[0]} and {cluster_ids[1]}:")
396 print(f" Original: {len(clusters)} clusters")
397 print(f" After merge: {len(merged)} clusters")
398
399
400def demo_integration():
401 """Demonstrate integration between containers."""
402 print("\n" + "=" * 70)
403 print("Container Integration Demo")
404 print("=" * 70)
405
406 # Create tracks and measurements
407 tracks = create_sample_tracks(n_tracks=20)
408 measurements = create_sample_measurements(n_times=10, n_per_time=15)
409
410 print(f"\nDataset: {len(tracks)} tracks, {len(measurements)} measurements")
411
412 # Filter to confirmed tracks
413 confirmed = tracks.confirmed
414 print(f"\nConfirmed tracks: {len(confirmed)}")
415
416 # For each confirmed track, find nearby measurements
417 print("\nMatching tracks to nearby measurements:")
418 for track in list(confirmed)[:3]: # Show first 3
419 pos = track.state[[0, 2]] # x, y position
420 nearby_meas = measurements.in_region(pos, radius=20.0)
421 print(
422 f" Track {track.id} at ({pos[0]:.1f}, {pos[1]:.1f}): "
423 f"{len(nearby_meas)} nearby measurements"
424 )
425
426 # Cluster confirmed tracks
427 if len(confirmed) >= 3:
428 clusters = ClusterSet.from_tracks(
429 confirmed,
430 method="dbscan",
431 eps=50.0,
432 min_samples=2,
433 )
434 print(f"\nClustered confirmed tracks: {len(clusters)} formations")
435
436 # For each cluster, find measurements near centroid
437 for cluster in clusters:
438 nearby = measurements.in_region(cluster.centroid, radius=30.0)
439 print(
440 f" Cluster {cluster.id} ({len(cluster.track_ids)} tracks): "
441 f"{len(nearby)} measurements near centroid"
442 )
443
444 # Time-synchronized analysis
445 print("\n--- Time-Synchronized Analysis ---")
446 for t in [0.0, 2.0, 4.0]:
447 meas_at_t = measurements.at_time(t)
448 tracks_at_t = tracks.filter_by_time(max_time=t + 1.0)
449 print(
450 f" t={t}: {len(meas_at_t)} measurements, "
451 f"{len(tracks_at_t)} tracks updated before t={t + 1}"
452 )
453
454
455def main():
456 """Run all demonstrations."""
457 print("\n" + "#" * 70)
458 print("# PyTCL Tracking Containers Example")
459 print("#" * 70)
460
461 demo_track_list()
462 demo_measurement_set()
463 demo_cluster_set()
464 demo_integration()
465
466 # Visualization
467 visualize_track_distribution()
468
469 print("\n" + "=" * 70)
470 print("Example complete!")
471 print("=" * 70)
472
473
474def visualize_track_distribution():
475 """Visualize track spatial distribution."""
476 print("\nGenerating track distribution visualization...")
477
478 # Create sample tracks
479 tracks = create_sample_tracks(n_tracks=15)
480
481 # Extract positions
482 positions = []
483 for track in tracks:
484 if track.state is not None:
485 # Assuming state is [x, vx, y, vy]
486 pos = track.state[[0, 2]]
487 positions.append(pos)
488
489 if positions:
490 positions = np.array(positions)
491
492 # Create scatter plot
493 fig = go.Figure()
494
495 fig.add_trace(
496 go.Scatter(
497 x=positions[:, 0],
498 y=positions[:, 1],
499 mode="markers+text",
500 text=[f"T{i}" for i in range(len(positions))],
501 marker=dict(size=10, color="blue", opacity=0.7),
502 textposition="top center",
503 name="Track Positions",
504 )
505 )
506
507 fig.update_layout(
508 title="Track Spatial Distribution",
509 xaxis_title="X Position (m)",
510 yaxis_title="Y Position (m)",
511 height=600,
512 width=700,
513 showlegend=False,
514 )
515
516 if SHOW_PLOTS:
517 fig.show()
518 else:
519 OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
520 fig.write_html(
521 str(OUTPUT_DIR / "tracking_containers.html"),
522 include_plotlyjs="cdn",
523 div_id="tracking_containers",
524 )
525
526
527if __name__ == "__main__":
528 main()
Running the Example
python examples/tracking_containers.py
See Also
Multi-Target Tracking - Using containers in MTT
Spatial Data Structures - Spatial indexing