Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
ed87881
TEP-90209: Tracked object contains re-id state and chain of object ids
saratpoluri Apr 11, 2026
7115171
Add another state called reid_disabled
saratpoluri Apr 11, 2026
aa27bca
Fix unique-id-count logic and update tests
saratpoluri Apr 12, 2026
1548a94
Apply copilot suggested changes to agent instructions
saratpoluri Apr 11, 2026
79c7a0f
Check re-id effectiveness with time chunking as well.
saratpoluri Apr 12, 2026
9d698ac
Handle no match but not null detection id
saratpoluri Apr 14, 2026
7077093
Fix indent failures
saratpoluri Apr 14, 2026
be28ebb
Prettier fixes
saratpoluri Apr 14, 2026
b5e672e
Fix two failing sensor tests. (#1290)
FilipAdamus97 Apr 13, 2026
8f3d25e
[CI/CD] Add login to DockerHub step for self-hosted runners (#1305)
dmytroye Apr 14, 2026
3afdc3a
Missing test files
saratpoluri Apr 15, 2026
4ae8ecf
Update .gitignore (#1307)
dmytroye Apr 15, 2026
2b501ab
pip: bump django from 5.2.12 to 5.2.13 in /manager (#1296)
dependabot[bot] Apr 16, 2026
adac212
[DLStreamer] Update to 2026.1.0-20260414-weekly-ubuntu24 (#1309)
scenescapecicd Apr 16, 2026
e3e9db4
pip: bump pytest from 9.0.2 to 9.0.3 in /.github/resources (#1303)
dependabot[bot] Apr 17, 2026
41fec6e
[ITEP-90163] [Tracker evaluation] Verify projecting camera accuracy o…
dmytroye Apr 17, 2026
99cca96
Fix the reid-unique-count test failures
saratpoluri Apr 21, 2026
6409044
Add bbox size as a configurable param
saratpoluri Apr 21, 2026
e06f17f
Remove two scenes from reid test to avoid cross-pollination
saratpoluri Apr 22, 2026
05453d3
[DOCS] Restructure chapters - further alignment (#1291)
wiwaszko-intel Apr 20, 2026
8edb217
ITEP-23442: Include dwell time in region updates topic (#1300)
saratpoluri Apr 20, 2026
280a9dc
Fix current CVEs (#1312)
dmytroye Apr 20, 2026
2ed30d1
[ITEP-90160] Fix Autocalibration Docker image size regression (#1316)
dmytroye Apr 20, 2026
5d1665f
pip: bump gdown from 5.2.1 to 5.2.2 in /autocalibration (#1318)
dependabot[bot] Apr 20, 2026
b33dce0
ITEP-89462 Refactor and add UT for controller analytics functions (#1…
tdorauintc Apr 20, 2026
42e329c
ITEP-89630 - Automate MQTT events hierarchy tests (#1255)
sbelhaik Apr 21, 2026
bb4544a
ITEP-84336: Fix incorrect camera pose for models generated with VGGT …
daddo-intel Apr 22, 2026
f8ec3d4
Fix documentation
saratpoluri Apr 22, 2026
ae9bf5a
Leverage feature slice size in controller
saratpoluri Apr 22, 2026
40cce06
Update unique_id_count inside lock
saratpoluri Apr 22, 2026
349490e
Revert "Apply copilot suggested changes to agent instructions"
saratpoluri Apr 22, 2026
6385447
Merge branch 'main' into feature/object-reided
saratpoluri Apr 22, 2026
89c93f6
Fix prettier issues
saratpoluri Apr 22, 2026
076244f
Address further code review comments
saratpoluri Apr 22, 2026
50a6928
Merge branch 'main' into feature/object-reided
saratpoluri Apr 22, 2026
e98f1c1
Shutdown old tracker upon update
saratpoluri Apr 22, 2026
ab0c3ee
Don't reinitialize tracker for change in reid settings
saratpoluri Apr 22, 2026
e551305
Fix prettier issues and hold lock in test to call updateActiveDict
saratpoluri Apr 22, 2026
78e28a0
Align deserialize with updateScene
saratpoluri Apr 22, 2026
04c6cf6
Minor code review changes
saratpoluri Apr 22, 2026
499bcaa
Merge branch 'main' into feature/object-reided
saratpoluri Apr 23, 2026
7e285f0
Merge branch 'main' into feature/object-reided
saratpoluri Apr 24, 2026
91fe02a
Merge branch 'main' into feature/object-reided
saratpoluri Apr 24, 2026
d069856
Merge branch 'main' into feature/object-reided
daddo-intel Apr 24, 2026
cb1b1c9
previous_id_chains, test fixes
saratpoluri Apr 27, 2026
64efa7f
Update testing.md
saratpoluri Apr 27, 2026
48488d8
ITEP-90808: Add cosine similarity as a configurable option
saratpoluri Apr 24, 2026
fdc404d
Update cases to include cosine
saratpoluri Apr 27, 2026
8962ab5
Address code review comments
saratpoluri Apr 27, 2026
fcf4b94
Merge remote-tracking branch 'origin/feature/object-reided' into feat…
Copilot Apr 27, 2026
2821224
Update test with new config settings for reid
saratpoluri Apr 27, 2026
c9987ea
Merge remote-tracking branch 'origin/main' into feature/support-cosine
Copilot Apr 28, 2026
db2250f
Fix prettier issues
saratpoluri Apr 28, 2026
2421e6b
Apply suggestions from code review
saratpoluri Apr 28, 2026
dcc03c6
Fix merging errors and set proper default based on metric chosen
saratpoluri Apr 28, 2026
9647f9c
Merge branch 'main' into feature/support-cosine
saratpoluri Apr 28, 2026
1b55d8c
Merge branch 'main' into feature/support-cosine
saratpoluri Apr 29, 2026
06e03f9
Restore SECRETSDIR fallback in logic-unit-tests target
Copilot Apr 29, 2026
51481b8
Address open code review comments: L2 default, IP tolerance, COSINE t…
Copilot Apr 29, 2026
694baf8
Additional code review fixes
saratpoluri Apr 29, 2026
7a13c44
Fix code review comments
saratpoluri Apr 30, 2026
422bccc
Merge branch 'main' into feature/support-cosine
saratpoluri Apr 30, 2026
4a18810
Merge branch 'main' into feature/support-cosine
saratpoluri Apr 30, 2026
ede8800
Fix for prettier-check
saratpoluri Apr 30, 2026
a29eda8
Additional fixes
saratpoluri Apr 30, 2026
13dc87a
Adjust ADRs
saratpoluri Apr 30, 2026
2799c0d
Fix unit test error
saratpoluri May 1, 2026
6f197c3
Merge branch 'main' into feature/support-cosine
saratpoluri May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions controller/config/reid-config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"similarity_metric": "L2",
"stale_feature_timeout_secs": 5.0,
"stale_feature_check_interval_secs": 1.0,
"feature_accumulation_threshold": 12,
Expand Down
1 change: 1 addition & 0 deletions controller/src/controller/data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from abc import ABC, abstractmethod
from pathlib import Path
import json

from scene_common import log
from scene_common.rest_client import RESTClient

Expand Down
52 changes: 52 additions & 0 deletions controller/src/controller/reid.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,59 @@

from abc import ABC, abstractmethod

import numpy as np

from scene_common import log

class ReIDDatabase(ABC):
def prepareReidDict(self, embedding_vector, dimensions=None,
normalize_embeddings=False):
"""Prepare a normalized/validated ReID payload from arbitrary vector shapes.

Supports vectors shaped as (N,), (1, N), or any array-like object by
flattening to 1D. If dimensions is None, dimensions are inferred from the
flattened vector length.
"""
if embedding_vector is None:
log.warning("prepareReidDict: Empty embedding vector, skipping this vector")
return None

vec_array = np.asarray(embedding_vector, dtype="float32").reshape(-1)
inferred_dimensions = int(vec_array.shape[0])
expected_dimensions = inferred_dimensions if dimensions is None else int(dimensions)

if inferred_dimensions != expected_dimensions:
log.warning(
f"prepareReidDict: Expected vector shape ({expected_dimensions},) but got {vec_array.shape}, skipping this vector")
return None

if not np.all(np.isfinite(vec_array)):
log.warning("prepareReidDict: Vector contains non-finite values, skipping this vector")
return None

if normalize_embeddings:
norm = np.linalg.norm(vec_array)
if not np.isfinite(norm) or norm == 0.0:
log.warning(f"prepareReidDict: Invalid vector norm ({norm}), skipping this vector")
return None
vec_array = vec_array / norm

return {
"embedded_vector": vec_array.astype("float32", copy=False),
"dimensions": expected_dimensions,
}

def prepareReidVector(self, reid_vector, dimensions,
normalize_embeddings=False):
"""Backward-compatible wrapper returning only the prepared vector."""
prepared_reid = self.prepareReidDict(
reid_vector,
dimensions,
normalize_embeddings=normalize_embeddings)
if prepared_reid is None:
return None
return prepared_reid["embedded_vector"]

@abstractmethod
def connect(self, hostname):
"""
Expand Down
8 changes: 5 additions & 3 deletions controller/src/controller/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@

from types import SimpleNamespace
from typing import Optional

import numpy as np

import robot_vision as rv
from controller.controller_mode import ControllerMode
from controller.moving_object import ChainData
from scene_common import log
from scene_common.camera import Camera
from scene_common.earth_lla import convertLLAToECEF, calculateTRSLocal2LLAFromSurfacePoints
from scene_common.geometry import Line, Point, Region, Tripwire, getRegionEvents, getTripwireEvents
from scene_common.geometry import Point, Region, Tripwire, getRegionEvents, getTripwireEvents
from scene_common.scene_model import SceneModel
from scene_common.timestamp import get_epoch_time, get_iso_time
from scene_common.transform import CameraPose
from scene_common.mesh_util import getMeshAxisAlignedProjectionToXY, createRegionMesh, createObjectMesh

from controller.controller_mode import ControllerMode
from controller.moving_object import ChainData
from controller.ilabs_tracking import IntelLabsTracking
from controller.time_chunking import TimeChunkedIntelLabsTracking, DEFAULT_CHUNKING_RATE_FPS
from controller.tracking import (MAX_UNRELIABLE_TIME,
Expand Down
186 changes: 159 additions & 27 deletions controller/src/controller/uuid_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,80 @@
import collections
import concurrent.futures
import threading
import math

import numpy as np

from controller.vdms_adapter import VDMSDatabase
from controller.vdms_adapter import VDMSDatabase, COSINE_SIMILARITY_TOLERANCE
from controller.moving_object import ReidState, MovingObject
from scene_common import log
from scene_common.timestamp import get_epoch_time

DEFAULT_DATABASE = "VDMS"
DEFAULT_SIMILARITY_THRESHOLD = 40
DEFAULT_SIMILARITY_THRESHOLD_L2 = 40.0
DEFAULT_SIMILARITY_THRESHOLD_COSINE = 0.5
DEFAULT_MINIMUM_BBOX_AREA = 5000
Comment thread
saratpoluri marked this conversation as resolved.
DEFAULT_MINIMUM_FEATURE_COUNT = 12
DEFAULT_FEATURE_SLICE_SIZE = 10
DEFAULT_MAX_QUERY_TIME = 4
DEFAULT_MAX_SIMILARITY_QUERIES_TRACKED = 10
DEFAULT_STALE_FEATURE_TIMEOUT_SECS = 5.0
DEFAULT_STALE_FEATURE_CHECK_INTERVAL_SECS = 1.0
DEFAULT_SIMILARITY_METRIC = "L2"
SUPPORTED_SIMILARITY_METRICS = {"COSINE", "L2"}
# Tolerance applied to the theoretical [-1, 1] IP score bounds to absorb
# float32 rounding errors from VDMS normalization and inner-product computation.
available_databases = {
"VDMS": VDMSDatabase,
}

class UUIDManager:
def _normalizeSimilarityMetric(self, metric):
normalized_metric = str(metric).strip().upper()
if normalized_metric not in SUPPORTED_SIMILARITY_METRICS:
log.warning(
f"Unsupported similarity_metric '{metric}', "
f"supported values are {sorted(SUPPORTED_SIMILARITY_METRICS)}; "
f"falling back to {DEFAULT_SIMILARITY_METRIC}")
return DEFAULT_SIMILARITY_METRIC
return normalized_metric

def _resolveDatabaseSimilarityMetric(self, configured_metric):
"""Translate controller-facing similarity metric to the VDMS descriptor metric."""
metric = self._normalizeSimilarityMetric(configured_metric)
if metric == "COSINE":
return "IP"
return metric

def _resolveDefaultSimilarityThreshold(self, similarity_metric):
"""Return the default threshold for the configured similarity metric."""
if self._normalizeSimilarityMetric(similarity_metric) == "COSINE":
return DEFAULT_SIMILARITY_THRESHOLD_COSINE
return DEFAULT_SIMILARITY_THRESHOLD_L2

def _validateSimilarityThreshold(self, similarity_threshold, similarity_metric):
"""Normalize and validate the configured threshold for the active metric."""
try:
normalized_threshold = float(similarity_threshold)
except (TypeError, ValueError) as err:
raise ValueError(
f"similarity_threshold must be a finite numeric value, got {similarity_threshold}") from err

if not math.isfinite(normalized_threshold):
raise ValueError(
f"similarity_threshold must be a finite numeric value, got {similarity_threshold}")

normalized_metric = self._normalizeSimilarityMetric(similarity_metric)
if normalized_metric == "COSINE":
if normalized_threshold < -1.0 or normalized_threshold > 1.0:
raise ValueError(
"similarity_threshold for COSINE must be within [-1.0, 1.0]")
return normalized_threshold

if normalized_threshold < 0.0:
raise ValueError("similarity_threshold for L2 must be non-negative")
return normalized_threshold

def __init__(self, database=DEFAULT_DATABASE, reid_config_data=None):
self.active_ids = {}
self.active_ids_lock = threading.Lock()
Expand All @@ -34,6 +86,7 @@ def __init__(self, database=DEFAULT_DATABASE, reid_config_data=None):
self.features_for_database_timestamps = {} # Track when features were added
self.quality_features = {}
self.unique_id_count = 0
self.stale_feature_timer = None

self.unique_id_count_lock = threading.Lock()
# ReID embedding dimensions are inferred from the first observed embedding.
Expand Down Expand Up @@ -79,12 +132,21 @@ def _applyReidConfig(self, reid_config_data=None):
'stale_feature_check_interval_secs', DEFAULT_STALE_FEATURE_CHECK_INTERVAL_SECS)
self.minimum_feature_count = reid_config_data.get(
'feature_accumulation_threshold', DEFAULT_MINIMUM_FEATURE_COUNT)
self.similarity_threshold = reid_config_data.get(
'similarity_threshold', DEFAULT_SIMILARITY_THRESHOLD)
self.similarity_metric = self._normalizeSimilarityMetric(reid_config_data.get(
'similarity_metric', DEFAULT_SIMILARITY_METRIC))
configured_similarity_threshold = reid_config_data.get('similarity_threshold')
if configured_similarity_threshold is None:
configured_similarity_threshold = self._resolveDefaultSimilarityThreshold(
self.similarity_metric)
self.similarity_threshold = self._validateSimilarityThreshold(
configured_similarity_threshold, self.similarity_metric)
self.minimum_bbox_area = reid_config_data.get(
'minimum_bbox_area', DEFAULT_MINIMUM_BBOX_AREA)
self.feature_slice_size = reid_config_data.get(
'feature_slice_size', DEFAULT_FEATURE_SLICE_SIZE)
if hasattr(self, 'reid_database') and self.reid_database is not None:
self.reid_database.similarity_metric = self._resolveDatabaseSimilarityMetric(
self.similarity_metric)

def _rescheduleStaleFeatureTimer(self):
"""Cancel any existing stale-feature timer and start a new one."""
Expand Down Expand Up @@ -448,49 +510,119 @@ def parseQueryResults(self, similarity_scores, threshold=None):
"""
Check database for any similar objects and return an ID and similarity score.
Uses a majority-vote strategy: a candidate UUID must appear in at least half of the
per-vector best matches whose distance is below the threshold to be accepted.
When multiple candidates qualify, the one with the lowest distance is returned.
per-vector best matches that pass the metric-specific threshold test to be accepted.
When multiple candidates qualify, the one with the best metric value is returned
according to descriptor semantics (highest for IP/COSINE, lowest for L2).

@param similarity_scores The similarity scores obtained from the database query
@param threshold The maximum distance between Re-ID vectors still considered
a valid match; defaults to self.similarity_threshold
@return database_id UUID of the matched entry if a majority-vote match is found;
otherwise None
@return similarity Minimum distance to the matched entry if found; otherwise None
@param threshold Similarity threshold interpreted according to metric semantics:
- L2-style distance: lower is better, candidate must be < threshold
- IP-style score: higher is better, candidate must be > threshold
@return database_id Returns the ID of the matched entry from the database if one
is found; otherwise, returns None
@return similarity Similarity value returned by VDMS (`_distance` field) for
the matched entry if one is found; otherwise, return None
"""
if threshold is None:
threshold = self.similarity_threshold

if not self._hasValidSimilarityScoreShape(similarity_scores):
log.warning(
"parseQueryResults: Invalid similarity_scores shape; expected list[list[entity]]. "
f"Received type={type(similarity_scores)}")
return None, None

if similarity_scores:
minimum_distances = [self._findMinimumDistance(entities)
metric_candidates = [self._findBestMetricCandidate(entities)
for entities in similarity_scores]
distances_below_threshold = [(uuid, distance) for (uuid, distance) in
minimum_distances if
distance is not None and distance < threshold]

if distances_below_threshold:
counter = collections.Counter(item[0] for item in distances_below_threshold)
qualifying_candidates = [(uuid, metric_value) for (uuid, metric_value) in
metric_candidates if
metric_value is not None and
self._isSimilarityMatch(metric_value, threshold)]
if qualifying_candidates:
counter = collections.Counter(item[0] for item in qualifying_candidates)
most_common_uuid, count = counter.most_common(1)[0]
if count >= (len(minimum_distances) / 2):
similarity = min(item[1] for item in distances_below_threshold
if item[0] == most_common_uuid)
if count >= (len(metric_candidates) / 2):
similarity = self._pickBestMetricValue(
[item[1] for item in qualifying_candidates if item[0] == most_common_uuid])
return most_common_uuid, similarity

return None, None

def _findMinimumDistance(self, entities):
def _hasValidSimilarityScoreShape(self, similarity_scores):
"""Validate that query results follow the strict list-of-lists contract."""
if not similarity_scores:
return True

if not isinstance(similarity_scores, list):
return False

return all(isinstance(item, list) for item in similarity_scores)

def _isHigherBetterMetric(self):
"""Return True when the configured descriptor metric uses higher-is-better semantics."""
metric = getattr(self.reid_database, 'similarity_metric', None)
if metric is None:
return False
return str(metric).strip().upper() == "IP"

def _isSimilarityMatch(self, metric_value, threshold):
"""Evaluate threshold semantics according to the active descriptor metric."""
if metric_value is None:
return False

if not math.isfinite(metric_value):
return False

if self._isHigherBetterMetric():
# For IP metrics, scores must lie within [-1, 1] (normalized embeddings).
# Allow a small tolerance to absorb float32 rounding from VDMS computation.
if metric_value < -(1.0 + COSINE_SIMILARITY_TOLERANCE) or metric_value > (1.0 + COSINE_SIMILARITY_TOLERANCE):
return False
return metric_value > threshold
return metric_value < threshold

def _pickBestMetricValue(self, metric_values):
"""Pick best metric value according to descriptor metric semantics."""
if not metric_values:
return None
if self._isHigherBetterMetric():
return max(metric_values)
return min(metric_values)

def _findBestMetricCandidate(self, entities):
"""
Find the uuid with the minimum distance and the corresponding distance value.
Find the best candidate uuid and metric value according to descriptor semantics.

VDMS returns entities sorted ascending by _distance (closest first), so entities[0]
is always the best match.
The best match is selected from the provided entities based on the configured
descriptor metric semantics: higher values are better for higher-is-better
metrics, and lower values are better otherwise.

Structure of entities:
[{'uuid': <UUID>, 'rvid': <TRACKER_ID>, '_distance': <SIMILARITY_SCORE>}, ...]
"""
is_higher_better = self._isHigherBetterMetric()
if entities:
minimum_distance_entity = entities[0]
return (minimum_distance_entity['uuid'], minimum_distance_entity['_distance'])
filtered_entities = []
for entity in entities:
metric_value = entity.get('_distance')
if metric_value is None or not math.isfinite(metric_value):
continue
if is_higher_better and (metric_value < -(1.0 + COSINE_SIMILARITY_TOLERANCE) or metric_value > (1.0 + COSINE_SIMILARITY_TOLERANCE)):
log.warning(
f"Ignoring out-of-range IP similarity score {metric_value} "
f"for uuid={entity.get('uuid')}")
continue
Comment thread
saratpoluri marked this conversation as resolved.
filtered_entities.append(entity)

if not filtered_entities:
return (None, None)

if is_higher_better:
best_entity = max(filtered_entities, key=lambda x: x['_distance'])
else:
best_entity = min(filtered_entities, key=lambda x: x['_distance'])
return (best_entity['uuid'], best_entity['_distance'])
return (None, None)

def _activeGidIndex(self):
Expand Down
Loading
Loading