|
1 |
| -import logging |
2 | 1 | import math
|
3 | 2 | from collections import defaultdict
|
4 | 3 | from collections.abc import Iterable
|
|
7 | 6 |
|
8 | 7 | from ami.main.models import Detection, Occurrence
|
9 | 8 |
|
10 |
| -logger = logging.getLogger(__name__) |
11 |
| - |
12 |
| -TRACKING_COST_THRESHOLD = 0.25 |
| 9 | +TRACKING_COST_THRESHOLD = 2 |
13 | 10 |
|
14 | 11 |
|
15 | 12 | def cosine_similarity(v1: Iterable[float], v2: Iterable[float]) -> float:
|
@@ -60,48 +57,68 @@ def total_cost(f1, f2, bb1, bb2, diag):
|
60 | 57 | )
|
61 | 58 |
|
62 | 59 |
|
| 60 | +def get_latest_feature_vector(detection: Detection): |
| 61 | + return ( |
| 62 | + detection.classifications.filter(features_2048__isnull=False) |
| 63 | + .order_by("-timestamp") |
| 64 | + .values_list("features_2048", flat=True) |
| 65 | + .first() |
| 66 | + ) |
| 67 | + |
| 68 | + |
63 | 69 | def assign_occurrences_by_tracking(
|
64 | 70 | detections: list[Detection],
|
65 |
| - logger: logging.Logger, |
| 71 | + logger, |
66 | 72 | ) -> None:
|
67 | 73 | """
|
68 | 74 | Perform object tracking by assigning detections across multiple source images
|
69 |
| - to the same Occurrence if they are similar enough. |
| 75 | + to the same Occurrence if they are similar enough, based on the latest classification feature vectors. |
70 | 76 | """
|
71 |
| - logger.info(f"Starting to assign occurrences by tracking.{len(detections)} detections found.") |
72 |
| - # Group detections by source image and sort |
| 77 | + logger.info(f"Starting to assign occurrences by tracking. {len(detections)} detections found.") |
| 78 | + |
| 79 | + # Group detections by source image timestamp |
73 | 80 | image_to_dets = defaultdict(list)
|
74 | 81 | for det in detections:
|
75 | 82 | image_to_dets[det.source_image.timestamp].append(det)
|
76 |
| - sorted_images = sorted(image_to_dets.keys()) |
77 |
| - logger.info(f"Found {len(sorted_images)} source images with detections.") |
| 83 | + sorted_timestamps = sorted(image_to_dets.keys()) |
| 84 | + logger.info(f"Found {len(sorted_timestamps)} source images with detections.") |
| 85 | + |
78 | 86 | last_detections = []
|
79 | 87 |
|
80 |
| - for t in sorted_images: |
81 |
| - current_detections = image_to_dets[t] |
82 |
| - logger.info(f"Processing {len(current_detections)} detections at {t}") |
| 88 | + for timestamp in sorted_timestamps: |
| 89 | + current_detections = image_to_dets[timestamp] |
| 90 | + logger.info(f"Processing {len(current_detections)} detections at {timestamp}") |
| 91 | + |
83 | 92 | for det in current_detections:
|
| 93 | + det_vec = get_latest_feature_vector(det) |
| 94 | + if det_vec is None: |
| 95 | + logger.info(f"No features for detection {det.id}, skipping.") |
| 96 | + continue |
| 97 | + |
84 | 98 | best_match = None
|
85 | 99 | best_cost = float("inf")
|
86 | 100 |
|
87 | 101 | for prev in last_detections:
|
88 |
| - if prev.similarity_vector is None or det.similarity_vector is None: |
| 102 | + prev_vec = get_latest_feature_vector(prev) |
| 103 | + if prev_vec is None: |
89 | 104 | continue
|
90 | 105 |
|
91 | 106 | cost = total_cost(
|
92 |
| - det.similarity_vector, |
93 |
| - prev.similarity_vector, |
| 107 | + det_vec, |
| 108 | + prev_vec, |
94 | 109 | det.bbox,
|
95 | 110 | prev.bbox,
|
96 | 111 | image_diagonal(det.source_image.width, det.source_image.height),
|
97 | 112 | )
|
98 | 113 |
|
| 114 | + logger.info(f"Comparing detection {det.id} with previous {prev.id}: cost = {cost:.4f}") |
99 | 115 | if cost < best_cost:
|
100 | 116 | best_cost = cost
|
101 | 117 | best_match = prev
|
102 | 118 |
|
103 | 119 | if best_match and best_cost < TRACKING_COST_THRESHOLD:
|
104 | 120 | det.occurrence = best_match.occurrence
|
| 121 | + logger.info(f"Assigned detection {det.id} to existing occurrence {best_match.occurrence.pk}") |
105 | 122 | else:
|
106 | 123 | occurrence = Occurrence.objects.create(event=det.source_image.event)
|
107 | 124 | det.occurrence = occurrence
|
|
0 commit comments