From 45bbe9dd2a0d11fd0d9612c832ef0efbf0baa2ba Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Mon, 8 Dec 2025 14:19:16 +0100 Subject: [PATCH 01/33] Initial commit --- .../src/controller/detections_builder.py | 4 + controller/src/controller/scene.py | 84 ++++++++++++++++++- controller/src/controller/scene_controller.py | 32 +++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index ea276e7f4..7e6635745 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -45,6 +45,10 @@ def prepareObjDict(scene, obj, update_visibility): 'size': aobj.size, 'velocity': velocity.asCartesianVector }) + + # Add frame count for analytics + if hasattr(aobj, 'frameCount'): + obj_dict['frame_count'] = aobj.frameCount rotation = aobj.rotation if rotation is not None: diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 37c91e539..2057a4e56 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 import itertools +from types import SimpleNamespace from typing import Optional import numpy as np @@ -57,6 +58,9 @@ def __init__(self, name, map_file, scale=None, self._setTracker("time_chunked_intel_labs" if time_chunking_enabled else self.DEFAULT_TRACKER) self._trs_xyz_to_lla = None self.use_tracker = True + + # Cache for tracked objects from MQTT (for analytics) + self.tracked_objects_cache = {} # FIXME - only for backwards compatibility self.scale = scale @@ -294,10 +298,88 @@ def processSensorData(self, jdata, when): return True + def updateTrackedObjects(self, detection_type, objects): + """ + Update the cache of tracked objects from MQTT. + This is used by Analytics to consume tracked objects published by the Tracker service. + + Args: + detection_type: The type of detection (e.g., 'person', 'vehicle') + objects: List of tracked objects for this detection type + """ + self.tracked_objects_cache[detection_type] = objects + return + + def getTrackedObjects(self, detection_type): + """ + Get tracked objects from cache (MQTT) or fallback to direct tracker call. + + Args: + detection_type: The type of detection + + Returns: + List of tracked objects (MovingObject instances or serialized dicts) + """ + # First try to get from cache (MQTT-based) + if detection_type in self.tracked_objects_cache: + cached_objects = self.tracked_objects_cache[detection_type] + # If cached objects are serialized dicts, we need to use them differently + # For now, return them - the analytics code will need to handle both formats + if cached_objects and isinstance(cached_objects[0], dict): + # Convert serialized objects back to a format compatible with MovingObject + # This is a simplified approach - ideally we'd recreate MovingObject instances + return self._deserializeTrackedObjects(cached_objects) + return cached_objects + + # Fallback to direct tracker call (for backward compatibility) + if self.tracker is not None: + return self.tracker.currentObjects(detection_type) + + return [] + + def _deserializeTrackedObjects(self, serialized_objects): + """ + Convert serialized tracked objects to a format usable by Analytics. + This creates lightweight wrappers that mimic MovingObject interface. + + Args: + serialized_objects: List of serialized object dictionaries + + Returns: + List of object-like structures with necessary attributes + """ + from types import SimpleNamespace + + objects = [] + for obj_data in serialized_objects: + # Create a simple object that has the necessary attributes + obj = SimpleNamespace() + obj.gid = obj_data.get('id') + obj.category = obj_data.get('type') + obj.sceneLoc = Point(obj_data.get('translation', [0, 0, 0])) + obj.velocity = Point(obj_data.get('velocity', [0, 0, 0])) if obj_data.get('velocity') else None + obj.size = obj_data.get('size') + obj.confidence = obj_data.get('confidence') + obj.frameCount = obj_data.get('frame_count', 0) + obj.when = get_epoch_time(obj_data.get('first_seen')) if 'first_seen' in obj_data else get_epoch_time() + obj.visibility = obj_data.get('visibility', []) + + # Chain data for regions, sensors, and published locations + obj.chain_data = SimpleNamespace() + obj.chain_data.regions = obj_data.get('regions', {}) + obj.chain_data.sensors = obj_data.get('sensors', {}) + obj.chain_data.persist = obj_data.get('persistent_data', {}) + # Initialize publishedLocations - will be populated by _updateEvents + obj.chain_data.publishedLocations = [obj.sceneLoc] + + objects.append(obj) + + return objects + def _updateEvents(self, detectionType, now): self.events = {} now_str = get_iso_time(now) - curObjects = self.tracker.currentObjects(detectionType) + curObjects = self.getTrackedObjects(detectionType) for obj in curObjects: obj.chain_data.publishedLocations.insert(0, obj.sceneLoc) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 768d7b026..1eab37460 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -428,6 +428,31 @@ def handleMovingObjectMessage(self, client, userdata, message): self.publishEvents(scene, jdata['timestamp']) return + def handleSceneDataMessage(self, client, userdata, message): + """ + Handle scene data messages (tracked objects) published to DATA_SCENE topic. + This updates the Analytics cache with tracked objects from the existing topic. + """ + topic = PubSub.parseTopic(message.topic) + jdata = orjson.loads(message.payload.decode('utf-8')) + + scene_id = topic['scene_id'] + detection_type = topic['thing_type'] + + scene = self.cache_manager.sceneWithID(scene_id) + if scene is None: + log.debug("Scene not found for tracked objects, ignoring", scene_id) + return + + # Extract tracked objects from the existing DATA_SCENE message + tracked_objects = jdata.get('objects', []) + + # Update the analytics cache with tracked objects + scene.updateTrackedObjects(detection_type, tracked_objects) + + log.debug(f"Updated tracked objects cache for scene {scene.name}, type {detection_type}, count {len(tracked_objects)}") + return + def _handleChildSceneObject(self, sender_id, jdata, detection_type, msg_when): sender = self.cache_manager.sceneWithID(sender_id) if sender is None: @@ -593,6 +618,13 @@ def updateSubscriptions(self): for sensor in scene.sensors: need_subscribe.add((PubSub.formatTopic(PubSub.DATA_SENSOR, sensor_id=sensor), self.handleSensorMessage)) + + # Subscribe to scene data (tracked objects) for Analytics to consume + # This reuses the existing DATA_SCENE topic that tracker already publishes to + need_subscribe.add((PubSub.formatTopic(PubSub.DATA_SCENE, + scene_id=scene.uid, thing_type="+"), + self.handleSceneDataMessage)) + if hasattr(scene, 'children'): child_scenes = self.cache_manager.data_source.getChildScenes(scene.uid) From 56e31ed3ea16c9709b1a4413289eb3f75acef071 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Mon, 8 Dec 2025 14:37:32 +0100 Subject: [PATCH 02/33] Add debug logging --- controller/src/controller/scene.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 2057a4e56..1af327e59 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -323,16 +323,14 @@ def getTrackedObjects(self, detection_type): # First try to get from cache (MQTT-based) if detection_type in self.tracked_objects_cache: cached_objects = self.tracked_objects_cache[detection_type] - # If cached objects are serialized dicts, we need to use them differently - # For now, return them - the analytics code will need to handle both formats if cached_objects and isinstance(cached_objects[0], dict): - # Convert serialized objects back to a format compatible with MovingObject - # This is a simplified approach - ideally we'd recreate MovingObject instances return self._deserializeTrackedObjects(cached_objects) + log.debug("Using cached tracked objects for detection type:", detection_type) return cached_objects # Fallback to direct tracker call (for backward compatibility) if self.tracker is not None: + log.debug("Using direct tracker call for detection type:", detection_type) return self.tracker.currentObjects(detection_type) return [] From 71381ca847278deade092d07ade6d8de07f302d0 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Tue, 9 Dec 2025 11:13:08 +0100 Subject: [PATCH 03/33] Phyton indent fix --- controller/src/controller/scene.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 1af327e59..e03870412 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -313,10 +313,10 @@ def updateTrackedObjects(self, detection_type, objects): def getTrackedObjects(self, detection_type): """ Get tracked objects from cache (MQTT) or fallback to direct tracker call. - + Args: detection_type: The type of detection - + Returns: List of tracked objects (MovingObject instances or serialized dicts) """ @@ -327,27 +327,27 @@ def getTrackedObjects(self, detection_type): return self._deserializeTrackedObjects(cached_objects) log.debug("Using cached tracked objects for detection type:", detection_type) return cached_objects - + # Fallback to direct tracker call (for backward compatibility) if self.tracker is not None: log.debug("Using direct tracker call for detection type:", detection_type) return self.tracker.currentObjects(detection_type) - + return [] def _deserializeTrackedObjects(self, serialized_objects): """ Convert serialized tracked objects to a format usable by Analytics. This creates lightweight wrappers that mimic MovingObject interface. - + Args: serialized_objects: List of serialized object dictionaries - + Returns: List of object-like structures with necessary attributes """ from types import SimpleNamespace - + objects = [] for obj_data in serialized_objects: # Create a simple object that has the necessary attributes @@ -361,7 +361,7 @@ def _deserializeTrackedObjects(self, serialized_objects): obj.frameCount = obj_data.get('frame_count', 0) obj.when = get_epoch_time(obj_data.get('first_seen')) if 'first_seen' in obj_data else get_epoch_time() obj.visibility = obj_data.get('visibility', []) - + # Chain data for regions, sensors, and published locations obj.chain_data = SimpleNamespace() obj.chain_data.regions = obj_data.get('regions', {}) @@ -369,9 +369,9 @@ def _deserializeTrackedObjects(self, serialized_objects): obj.chain_data.persist = obj_data.get('persistent_data', {}) # Initialize publishedLocations - will be populated by _updateEvents obj.chain_data.publishedLocations = [obj.sceneLoc] - + objects.append(obj) - + return objects def _updateEvents(self, detectionType, now): From 1211d39a4978739effbc21678a2badb569e0d978 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Tue, 9 Dec 2025 11:21:38 +0100 Subject: [PATCH 04/33] Phyton indent fix --- controller/src/controller/detections_builder.py | 6 ++---- controller/src/controller/scene.py | 2 +- controller/src/controller/scene_controller.py | 14 +++++++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index 7e6635745..8ddd204f8 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -45,12 +45,10 @@ def prepareObjDict(scene, obj, update_visibility): 'size': aobj.size, 'velocity': velocity.asCartesianVector }) - + # Add frame count for analytics if hasattr(aobj, 'frameCount'): - obj_dict['frame_count'] = aobj.frameCount - - rotation = aobj.rotation + obj_dict['frame_count'] = aobj.frameCount rotation = aobj.rotation if rotation is not None: obj_dict['rotation'] = rotation diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index e03870412..137e4ffd4 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -58,7 +58,7 @@ def __init__(self, name, map_file, scale=None, self._setTracker("time_chunked_intel_labs" if time_chunking_enabled else self.DEFAULT_TRACKER) self._trs_xyz_to_lla = None self.use_tracker = True - + # Cache for tracked objects from MQTT (for analytics) self.tracked_objects_cache = {} diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 1eab37460..2e03fb326 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -435,21 +435,21 @@ def handleSceneDataMessage(self, client, userdata, message): """ topic = PubSub.parseTopic(message.topic) jdata = orjson.loads(message.payload.decode('utf-8')) - + scene_id = topic['scene_id'] detection_type = topic['thing_type'] - + scene = self.cache_manager.sceneWithID(scene_id) if scene is None: log.debug("Scene not found for tracked objects, ignoring", scene_id) return - + # Extract tracked objects from the existing DATA_SCENE message tracked_objects = jdata.get('objects', []) - + # Update the analytics cache with tracked objects scene.updateTrackedObjects(detection_type, tracked_objects) - + log.debug(f"Updated tracked objects cache for scene {scene.name}, type {detection_type}, count {len(tracked_objects)}") return @@ -618,13 +618,13 @@ def updateSubscriptions(self): for sensor in scene.sensors: need_subscribe.add((PubSub.formatTopic(PubSub.DATA_SENSOR, sensor_id=sensor), self.handleSensorMessage)) - + # Subscribe to scene data (tracked objects) for Analytics to consume # This reuses the existing DATA_SCENE topic that tracker already publishes to need_subscribe.add((PubSub.formatTopic(PubSub.DATA_SCENE, scene_id=scene.uid, thing_type="+"), self.handleSceneDataMessage)) - + if hasattr(scene, 'children'): child_scenes = self.cache_manager.data_source.getChildScenes(scene.uid) From 070cfbd0570e794f3d9a7a9f2f07381116b9e087 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Tue, 9 Dec 2025 11:26:40 +0100 Subject: [PATCH 05/33] Phyton indent fix --- controller/src/controller/scene.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 137e4ffd4..9c9054c94 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -302,7 +302,7 @@ def updateTrackedObjects(self, detection_type, objects): """ Update the cache of tracked objects from MQTT. This is used by Analytics to consume tracked objects published by the Tracker service. - + Args: detection_type: The type of detection (e.g., 'person', 'vehicle') objects: List of tracked objects for this detection type @@ -342,7 +342,7 @@ def _deserializeTrackedObjects(self, serialized_objects): Args: serialized_objects: List of serialized object dictionaries - + Returns: List of object-like structures with necessary attributes """ From 57c1b6f9527a859dfbdb4958d5ed0ba10bd9281c Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Tue, 9 Dec 2025 15:15:26 +0100 Subject: [PATCH 06/33] Fix SyntaxError --- controller/src/controller/detections_builder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index 8ddd204f8..ef1467ed1 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -48,7 +48,9 @@ def prepareObjDict(scene, obj, update_visibility): # Add frame count for analytics if hasattr(aobj, 'frameCount'): - obj_dict['frame_count'] = aobj.frameCount rotation = aobj.rotation + obj_dict['frame_count'] = aobj.frameCount + + rotation = aobj.rotation if rotation is not None: obj_dict['rotation'] = rotation From 1e97534d331fb0c033d1a90461788c1dce356f69 Mon Sep 17 00:00:00 2001 From: Dmytro Yermolenko Date: Thu, 11 Dec 2025 13:58:19 +0100 Subject: [PATCH 07/33] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- controller/src/controller/scene.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 9c9054c94..5e393f328 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -323,7 +323,7 @@ def getTrackedObjects(self, detection_type): # First try to get from cache (MQTT-based) if detection_type in self.tracked_objects_cache: cached_objects = self.tracked_objects_cache[detection_type] - if cached_objects and isinstance(cached_objects[0], dict): + if isinstance(cached_objects, list) and len(cached_objects) > 0 and isinstance(cached_objects[0], dict): return self._deserializeTrackedObjects(cached_objects) log.debug("Using cached tracked objects for detection type:", detection_type) return cached_objects @@ -346,7 +346,6 @@ def _deserializeTrackedObjects(self, serialized_objects): Returns: List of object-like structures with necessary attributes """ - from types import SimpleNamespace objects = [] for obj_data in serialized_objects: From 6c93325265fc213d1331d167e438258a68950038 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Mon, 15 Dec 2025 10:49:39 +0100 Subject: [PATCH 08/33] Add flag to disable tracker in controller --- controller/src/controller-cmd | 4 +- controller/src/controller/cache_manager.py | 5 +- controller/src/controller/scene.py | 47 ++++++++++++++----- controller/src/controller/scene_controller.py | 16 +++++-- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/controller/src/controller-cmd b/controller/src/controller-cmd index f5b166840..025764cb7 100755 --- a/controller/src/controller-cmd +++ b/controller/src/controller-cmd @@ -43,6 +43,8 @@ def build_argparser(): parser.add_argument("--visibility_topic", help="Which topic to publish visibility on." "Valid options are 'unregulated', 'regulated', or 'none'", default="regulated") + parser.add_argument("--disable-tracker", action="store_true", + help="Disable tracker functionality. Use this when running a separate Tracker service that produces data to MQTT.") return parser def main(): @@ -54,7 +56,7 @@ def main(): args.brokerauth, args.resturl, args.restauth, args.cert, args.rootcert, args.ntp, args.tracker_config_file, args.schema_file, - args.visibility_topic, args.data_source) + args.visibility_topic, args.data_source, args.disable_tracker) controller.loopForever() return diff --git a/controller/src/controller/cache_manager.py b/controller/src/controller/cache_manager.py index 468f79955..610d95104 100644 --- a/controller/src/controller/cache_manager.py +++ b/controller/src/controller/cache_manager.py @@ -11,10 +11,11 @@ class CacheManager: def __init__(self, data_source=None, rest_url=None, rest_auth=None, - root_cert=None, tracker_config_data={}): + root_cert=None, tracker_config_data={}, disable_tracker=False): self.cached_child_transforms_by_uid = {} self.camera_parameters = {} self.tracker_config_data = tracker_config_data + self.disable_tracker = disable_tracker self.cached_scenes_by_uid = {} self._cached_scenes_by_cameraID = {} self._cached_scenes_by_sensorID = {} @@ -58,7 +59,7 @@ def refreshScenes(self): uid = scene_data['uid'] if uid not in self.cached_scenes_by_uid: - scene = Scene.deserialize(scene_data) + scene = Scene.deserialize(scene_data, self.disable_tracker) else: scene = self.cached_scenes_by_uid[uid] scene.updateScene(scene_data) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 5e393f328..3b3da65ee 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -43,9 +43,11 @@ def __init__(self, name, map_file, scale=None, non_measurement_time_dynamic = NON_MEASUREMENT_TIME_DYNAMIC, non_measurement_time_static = NON_MEASUREMENT_TIME_STATIC, time_chunking_enabled = False, - time_chunking_interval_milliseconds = DEFAULT_CHUNKING_INTERVAL_MS): + time_chunking_interval_milliseconds = DEFAULT_CHUNKING_INTERVAL_MS, + disable_tracker = False): log.info("NEW SCENE", name, map_file, scale, max_unreliable_time, - non_measurement_time_dynamic, non_measurement_time_static) + non_measurement_time_dynamic, non_measurement_time_static, + "tracker_disabled=" + str(disable_tracker)) super().__init__(name, map_file, scale) self.ref_camera_frame_rate = None self.max_unreliable_time = max_unreliable_time @@ -55,9 +57,15 @@ def __init__(self, name, map_file, scale=None, self.trackerType = None self.persist_attributes = {} self.time_chunking_interval_milliseconds = time_chunking_interval_milliseconds - self._setTracker("time_chunked_intel_labs" if time_chunking_enabled else self.DEFAULT_TRACKER) + self.disable_tracker = disable_tracker + + if not disable_tracker: + self._setTracker("time_chunked_intel_labs" if time_chunking_enabled else self.DEFAULT_TRACKER) + else: + log.info("Tracker initialization SKIPPED for scene: " + name) + self._trs_xyz_to_lla = None - self.use_tracker = True + self.use_tracker = not disable_tracker # Cache for tracked objects from MQTT (for analytics) self.tracked_objects_cache = {} @@ -157,6 +165,12 @@ def processCameraData(self, jdata, when=None, ignoreTimeFlag=False): if not hasattr(camera, 'pose'): log.info("DISCARDING: camera has no pose") return True + + # Skip processing if tracker is disabled - data should come from separate Tracker service via MQTT + if self.disable_tracker: + log.debug(f"Tracker disabled, skipping camera data processing for camera {camera_id}") + return True + for detection_type, detections in jdata['objects'].items(): if "intrinsics" not in jdata: self._convertPixelBoundingBoxesToMeters(detections, camera.pose.intrinsics.intrinsics, camera.pose.intrinsics.distortion) @@ -218,6 +232,11 @@ def processSceneData(self, jdata, child, cameraPose, if 'frame_rate' in jdata: self.ref_camera_frame_rate = min(jdata['frame_rate'], self.ref_camera_frame_rate) if self.ref_camera_frame_rate is not None else jdata["frame_rate"] + # Skip processing if tracker is disabled + if self.disable_tracker: + log.debug(f"Tracker disabled, skipping scene data processing for child {child.name if hasattr(child, 'name') else 'unknown'}") + return True + objects = [] child_objects = [] for info in new: @@ -248,12 +267,13 @@ def processSceneData(self, jdata, child, cameraPose, def _finishProcessing(self, detectionType, when, objects, already_tracked_objects=[]): self._updateVisible(objects) - self.tracker.trackObjects(objects, already_tracked_objects, when, [detectionType], - self.ref_camera_frame_rate, - self.max_unreliable_time, - self.non_measurement_time_dynamic, - self.non_measurement_time_static, - self.use_tracker) + if not self.disable_tracker and self.tracker is not None: + self.tracker.trackObjects(objects, already_tracked_objects, when, [detectionType], + self.ref_camera_frame_rate, + self.max_unreliable_time, + self.non_measurement_time_dynamic, + self.non_measurement_time_static, + self.use_tracker) self._updateEvents(detectionType, when) return @@ -505,14 +525,15 @@ def _updateVisible(self, curObjects): return @classmethod - def deserialize(cls, data): + def deserialize(cls, data, disable_tracker=False): tracker_config = data.get('tracker_config', []) scene = cls(data['name'], data.get('map', None), data.get('scale', None), - *tracker_config) + *tracker_config, disable_tracker) scene.uid = data['uid'] scene.mesh_translation = data.get('mesh_translation', None) scene.mesh_rotation = data.get('mesh_rotation', None) - scene.use_tracker = data.get('use_tracker', True) + scene.use_tracker = data.get('use_tracker', True) and not disable_tracker + scene.disable_tracker = disable_tracker scene.output_lla = data.get('output_lla', None) scene.map_corners_lla = data.get('map_corners_lla', None) scene.retrack = data.get('retrack', True) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 2e03fb326..5339ca20f 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -27,7 +27,7 @@ class SceneController: def __init__(self, rewrite_bad_time, rewrite_all_time, max_lag, mqtt_broker, mqtt_auth, rest_url, rest_auth, client_cert, root_cert, ntp_server, - tracker_config_file, schema_file, visibility_topic, data_source): + tracker_config_file, schema_file, visibility_topic, data_source, disable_tracker=False): self.cert = client_cert self.root_cert = root_cert self.rewrite_bad_time = rewrite_bad_time @@ -38,8 +38,16 @@ def __init__(self, rewrite_bad_time, rewrite_all_time, max_lag, mqtt_broker, self.mqtt_auth = mqtt_auth self.tracker_config_data = {} self.tracker_config_file = tracker_config_file - if tracker_config_file is not None: - self.extractTrackerConfigData(tracker_config_file) + self.disable_tracker = disable_tracker + + if disable_tracker: + log.info("Tracker is DISABLED. Controller will run without tracker functionality.") + # Still load tracker config for analytics and other purposes + if tracker_config_file is not None: + self.extractTrackerConfigData(tracker_config_file) + else: + if tracker_config_file is not None: + self.extractTrackerConfigData(tracker_config_file) self.last_time_sync = None self.ntp_server = ntp_server @@ -52,7 +60,7 @@ def __init__(self, rewrite_bad_time, rewrite_all_time, max_lag, mqtt_broker, self.pubsub.onConnect = self.onConnect self.pubsub.connect() - self.cache_manager = CacheManager(data_source, rest_url, rest_auth, root_cert, self.tracker_config_data) + self.cache_manager = CacheManager(data_source, rest_url, rest_auth, root_cert, self.tracker_config_data, self.disable_tracker) self.visibility_topic = visibility_topic log.info(f"Publishing camera visibility info on {self.visibility_topic} topic.") From 93196d937ce97a8f0e8c8cfa0377d72acb105851 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Mon, 15 Dec 2025 10:51:37 +0100 Subject: [PATCH 09/33] Add flag to demo example docker-compose --- sample_data/docker-compose-dl-streamer-example.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sample_data/docker-compose-dl-streamer-example.yml b/sample_data/docker-compose-dl-streamer-example.yml index b72f1e75a..bdad540e8 100644 --- a/sample_data/docker-compose-dl-streamer-example.yml +++ b/sample_data/docker-compose-dl-streamer-example.yml @@ -165,11 +165,13 @@ services: - CONTROLLER_ENABLE_TRACING - CONTROLLER_TRACING_ENDPOINT - CONTROLLER_TRACING_SAMPLE_RATIO + - CONTROLLER_DISABLE_TRACKER=${CONTROLLER_DISABLE_TRACKER:-false} command: > --restauth /run/secrets/controller.auth --brokerauth /run/secrets/controller.auth --broker broker.scenescape.intel.com --ntp ntpserv + ${CONTROLLER_DISABLE_TRACKER:+--disable-tracker} # mount the trackerconfig file to the container configs: - source: tracker-config From 45d6b915c4e442923ed2e02115132bf13ec6f438 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Mon, 15 Dec 2025 11:34:03 +0100 Subject: [PATCH 10/33] Indent fix --- controller/src/controller/scene.py | 8 ++++---- controller/src/controller/scene_controller.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 3b3da65ee..0ffb55531 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -58,12 +58,12 @@ def __init__(self, name, map_file, scale=None, self.persist_attributes = {} self.time_chunking_interval_milliseconds = time_chunking_interval_milliseconds self.disable_tracker = disable_tracker - + if not disable_tracker: self._setTracker("time_chunked_intel_labs" if time_chunking_enabled else self.DEFAULT_TRACKER) else: log.info("Tracker initialization SKIPPED for scene: " + name) - + self._trs_xyz_to_lla = None self.use_tracker = not disable_tracker @@ -165,12 +165,12 @@ def processCameraData(self, jdata, when=None, ignoreTimeFlag=False): if not hasattr(camera, 'pose'): log.info("DISCARDING: camera has no pose") return True - + # Skip processing if tracker is disabled - data should come from separate Tracker service via MQTT if self.disable_tracker: log.debug(f"Tracker disabled, skipping camera data processing for camera {camera_id}") return True - + for detection_type, detections in jdata['objects'].items(): if "intrinsics" not in jdata: self._convertPixelBoundingBoxesToMeters(detections, camera.pose.intrinsics.intrinsics, camera.pose.intrinsics.distortion) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 5339ca20f..2eb4be85e 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -39,7 +39,7 @@ def __init__(self, rewrite_bad_time, rewrite_all_time, max_lag, mqtt_broker, self.tracker_config_data = {} self.tracker_config_file = tracker_config_file self.disable_tracker = disable_tracker - + if disable_tracker: log.info("Tracker is DISABLED. Controller will run without tracker functionality.") # Still load tracker config for analytics and other purposes From ef542a9ad771ca2c3c9471a270240b564d793724 Mon Sep 17 00:00:00 2001 From: Dmytro Yermolenko Date: Mon, 15 Dec 2025 11:55:28 +0100 Subject: [PATCH 11/33] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- controller/src/controller/scene.py | 13 +++++++++---- controller/src/controller/scene_controller.py | 8 +++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 0ffb55531..4596accf5 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -267,7 +267,7 @@ def processSceneData(self, jdata, child, cameraPose, def _finishProcessing(self, detectionType, when, objects, already_tracked_objects=[]): self._updateVisible(objects) - if not self.disable_tracker and self.tracker is not None: + if not self.disable_tracker: self.tracker.trackObjects(objects, already_tracked_objects, when, [detectionType], self.ref_camera_frame_rate, self.max_unreliable_time, @@ -345,8 +345,9 @@ def getTrackedObjects(self, detection_type): cached_objects = self.tracked_objects_cache[detection_type] if isinstance(cached_objects, list) and len(cached_objects) > 0 and isinstance(cached_objects[0], dict): return self._deserializeTrackedObjects(cached_objects) - log.debug("Using cached tracked objects for detection type:", detection_type) - return cached_objects + else: + log.debug("Using cached tracked objects for detection type:", detection_type) + return cached_objects # Fallback to direct tracker call (for backward compatibility) if self.tracker is not None: @@ -378,7 +379,11 @@ def _deserializeTrackedObjects(self, serialized_objects): obj.size = obj_data.get('size') obj.confidence = obj_data.get('confidence') obj.frameCount = obj_data.get('frame_count', 0) - obj.when = get_epoch_time(obj_data.get('first_seen')) if 'first_seen' in obj_data else get_epoch_time() + if 'first_seen' in obj_data: + obj.when = get_epoch_time(obj_data.get('first_seen')) + else: + obj.when = None + log.warning(f"Missing 'first_seen' for object id {obj_data.get('id')}; setting obj.when to None.") obj.visibility = obj_data.get('visibility', []) # Chain data for regions, sensors, and published locations diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 2eb4be85e..9b0be48d2 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -43,12 +43,10 @@ def __init__(self, rewrite_bad_time, rewrite_all_time, max_lag, mqtt_broker, if disable_tracker: log.info("Tracker is DISABLED. Controller will run without tracker functionality.") # Still load tracker config for analytics and other purposes - if tracker_config_file is not None: - self.extractTrackerConfigData(tracker_config_file) - else: - if tracker_config_file is not None: - self.extractTrackerConfigData(tracker_config_file) + pass + if tracker_config_file is not None: + self.extractTrackerConfigData(tracker_config_file) self.last_time_sync = None self.ntp_server = ntp_server self.ntp_client = ntplib.NTPClient() From c146c78b3aa6d21077ae8d37dcade7d3d82baba5 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Wed, 17 Dec 2025 12:22:52 +0100 Subject: [PATCH 12/33] Dissable tracker flag --- sample_data/docker-compose-dl-streamer-example.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sample_data/docker-compose-dl-streamer-example.yml b/sample_data/docker-compose-dl-streamer-example.yml index bdad540e8..60cfb4e20 100644 --- a/sample_data/docker-compose-dl-streamer-example.yml +++ b/sample_data/docker-compose-dl-streamer-example.yml @@ -165,13 +165,12 @@ services: - CONTROLLER_ENABLE_TRACING - CONTROLLER_TRACING_ENDPOINT - CONTROLLER_TRACING_SAMPLE_RATIO - - CONTROLLER_DISABLE_TRACKER=${CONTROLLER_DISABLE_TRACKER:-false} command: > --restauth /run/secrets/controller.auth --brokerauth /run/secrets/controller.auth --broker broker.scenescape.intel.com --ntp ntpserv - ${CONTROLLER_DISABLE_TRACKER:+--disable-tracker} + # --disable-tracker # mount the trackerconfig file to the container configs: - source: tracker-config From 38a9bbbfad45731507aa29e9913ddeaae7fc8280 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Wed, 17 Dec 2025 12:36:05 +0100 Subject: [PATCH 13/33] If tracker is enabled, use direct tracker call --- controller/src/controller/scene.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 4596accf5..baeaf2b63 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -332,7 +332,7 @@ def updateTrackedObjects(self, detection_type, objects): def getTrackedObjects(self, detection_type): """ - Get tracked objects from cache (MQTT) or fallback to direct tracker call. + Get tracked objects from cache (MQTT) or direct tracker call. Args: detection_type: The type of detection @@ -340,16 +340,18 @@ def getTrackedObjects(self, detection_type): Returns: List of tracked objects (MovingObject instances or serialized dicts) """ - # First try to get from cache (MQTT-based) - if detection_type in self.tracked_objects_cache: - cached_objects = self.tracked_objects_cache[detection_type] - if isinstance(cached_objects, list) and len(cached_objects) > 0 and isinstance(cached_objects[0], dict): - return self._deserializeTrackedObjects(cached_objects) - else: - log.debug("Using cached tracked objects for detection type:", detection_type) - return cached_objects - - # Fallback to direct tracker call (for backward compatibility) + # If tracker is disabled, only use MQTT cache (from separate Tracker service) + if self.disable_tracker: + if detection_type in self.tracked_objects_cache: + cached_objects = self.tracked_objects_cache[detection_type] + if isinstance(cached_objects, list) and len(cached_objects) > 0 and isinstance(cached_objects[0], dict): + return self._deserializeTrackedObjects(cached_objects) + else: + log.debug("Using cached tracked objects from MQTT for detection type:", detection_type) + return cached_objects + return [] + + # If tracker is enabled, use direct tracker call (traditional mode) if self.tracker is not None: log.debug("Using direct tracker call for detection type:", detection_type) return self.tracker.currentObjects(detection_type) From 5922c7c3cda82902bf9e7c5edc177bce080ba5c3 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Wed, 17 Dec 2025 14:31:53 +0100 Subject: [PATCH 14/33] Phyton ident --- controller/src/controller/scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index baeaf2b63..389af7931 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -350,7 +350,7 @@ def getTrackedObjects(self, detection_type): log.debug("Using cached tracked objects from MQTT for detection type:", detection_type) return cached_objects return [] - + # If tracker is enabled, use direct tracker call (traditional mode) if self.tracker is not None: log.debug("Using direct tracker call for detection type:", detection_type) From 05b101fa0fd0bfc0e529aeed7f8c11190ff2ebf5 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Wed, 17 Dec 2025 16:12:00 +0100 Subject: [PATCH 15/33] Fix errors on MQTT messeges --- controller/src/controller/scene.py | 25 +++++++++++++++++- controller/src/controller/scene_controller.py | 26 ++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 389af7931..c02a1bc49 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -375,19 +375,42 @@ def _deserializeTrackedObjects(self, serialized_objects): # Create a simple object that has the necessary attributes obj = SimpleNamespace() obj.gid = obj_data.get('id') - obj.category = obj_data.get('type') + obj.category = obj_data.get('type', obj_data.get('category')) obj.sceneLoc = Point(obj_data.get('translation', [0, 0, 0])) obj.velocity = Point(obj_data.get('velocity', [0, 0, 0])) if obj_data.get('velocity') else None obj.size = obj_data.get('size') obj.confidence = obj_data.get('confidence') obj.frameCount = obj_data.get('frame_count', 0) + obj.rotation = obj_data.get('rotation') + obj.reidVector = obj_data.get('reid') + obj.similarity = obj_data.get('similarity') + obj.asset_scale = obj_data.get('asset_scale') + obj.vectors = [] # Empty list - tracked objects from MQTT don't have detection vectors + obj.boundingBoxPixels = None # Will use camera_bounds from obj_data if available + if 'first_seen' in obj_data: obj.when = get_epoch_time(obj_data.get('first_seen')) + obj.first_seen = obj.when else: obj.when = None + obj.first_seen = None log.warning(f"Missing 'first_seen' for object id {obj_data.get('id')}; setting obj.when to None.") obj.visibility = obj_data.get('visibility', []) + # Create info dict with original object data (needed by prepareObjDict) + obj.info = { + 'category': obj.category, + 'confidence': obj.confidence, + } + + # Add center_of_mass if available + if 'center_of_mass' in obj_data: + obj.info['center_of_mass'] = obj_data['center_of_mass'] + + # Add camera_bounds if available + if 'camera_bounds' in obj_data: + obj.info['camera_bounds'] = obj_data['camera_bounds'] + # Chain data for regions, sensors, and published locations obj.chain_data = SimpleNamespace() obj.chain_data.regions = obj_data.get('regions', {}) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 9b0be48d2..5035e9f77 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -438,6 +438,7 @@ def handleSceneDataMessage(self, client, userdata, message): """ Handle scene data messages (tracked objects) published to DATA_SCENE topic. This updates the Analytics cache with tracked objects from the existing topic. + When tracker is disabled, this also publishes analytics results. """ topic = PubSub.parseTopic(message.topic) jdata = orjson.loads(message.payload.decode('utf-8')) @@ -447,16 +448,34 @@ def handleSceneDataMessage(self, client, userdata, message): scene = self.cache_manager.sceneWithID(scene_id) if scene is None: - log.debug("Scene not found for tracked objects, ignoring", scene_id) + log.warning(f"Scene not found for tracked objects, ignoring scene_id={scene_id}") return # Extract tracked objects from the existing DATA_SCENE message tracked_objects = jdata.get('objects', []) + log.info(f"Received scene data message: scene={scene.name}, type={detection_type}, object_count={len(tracked_objects)}, disable_tracker={self.disable_tracker}") + # Update the analytics cache with tracked objects scene.updateTrackedObjects(detection_type, tracked_objects) - log.debug(f"Updated tracked objects cache for scene {scene.name}, type {detection_type}, count {len(tracked_objects)}") + # When tracker is disabled, we need to publish analytics based on tracked objects from MQTT + if self.disable_tracker: + log.info(f"Tracker disabled - processing analytics for scene {scene.name}") + + # Get tracked objects that Analytics will use + analytics_objects = scene.getTrackedObjects(detection_type) + log.info(f"Retrieved {len(analytics_objects)} tracked objects for analytics") + + # Prepare message data for publishing + msg_when = get_epoch_time(jdata.get('timestamp')) + + # Publish detections using the tracked objects + self.publishDetections(scene, analytics_objects, msg_when, detection_type, jdata, None) + self.publishEvents(scene, jdata.get('timestamp')) + + log.info(f"Published analytics for scene {scene.name}, type={detection_type}, count={len(analytics_objects)}") + return def _handleChildSceneObject(self, sender_id, jdata, detection_type, msg_when): @@ -540,7 +559,8 @@ def updateObjectClasses(self): results = self.cache_manager.data_source.getAssets() if results and 'results' in results: for scene in self.scenes: - scene.tracker.updateObjectClasses(results['results']) + if scene.tracker is not None: + scene.tracker.updateObjectClasses(results['results']) return def updateTRSMatrix(self): From 45b6dc22cefd207377d4272160e29cce9580adf6 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Wed, 17 Dec 2025 17:05:54 +0100 Subject: [PATCH 16/33] Remove logs and debug changes --- controller/src/controller/detections_builder.py | 2 +- controller/src/controller/scene_controller.py | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index ef1467ed1..d200e5195 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -95,7 +95,7 @@ def computeCameraBounds(scene, aobj, obj_dict): camera_bounds = {} for cameraID in obj_dict['visibility']: bounds = None - if aobj and hasattr(aobj.vectors[0].camera, 'cameraID') \ + if aobj and len(aobj.vectors) > 0 and hasattr(aobj.vectors[0].camera, 'cameraID') \ and cameraID == aobj.vectors[0].camera.cameraID: bounds = getattr(aobj, 'boundingBoxPixels', None) elif scene: diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 5035e9f77..220a450b5 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -454,18 +454,13 @@ def handleSceneDataMessage(self, client, userdata, message): # Extract tracked objects from the existing DATA_SCENE message tracked_objects = jdata.get('objects', []) - log.info(f"Received scene data message: scene={scene.name}, type={detection_type}, object_count={len(tracked_objects)}, disable_tracker={self.disable_tracker}") - # Update the analytics cache with tracked objects scene.updateTrackedObjects(detection_type, tracked_objects) # When tracker is disabled, we need to publish analytics based on tracked objects from MQTT if self.disable_tracker: - log.info(f"Tracker disabled - processing analytics for scene {scene.name}") - # Get tracked objects that Analytics will use analytics_objects = scene.getTrackedObjects(detection_type) - log.info(f"Retrieved {len(analytics_objects)} tracked objects for analytics") # Prepare message data for publishing msg_when = get_epoch_time(jdata.get('timestamp')) @@ -473,8 +468,6 @@ def handleSceneDataMessage(self, client, userdata, message): # Publish detections using the tracked objects self.publishDetections(scene, analytics_objects, msg_when, detection_type, jdata, None) self.publishEvents(scene, jdata.get('timestamp')) - - log.info(f"Published analytics for scene {scene.name}, type={detection_type}, count={len(analytics_objects)}") return From 4359918244ccb905436a0d5d34a5da6a8a658b81 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Wed, 17 Dec 2025 17:12:41 +0100 Subject: [PATCH 17/33] Whitespaces --- controller/src/controller/scene.py | 6 +++--- controller/src/controller/scene_controller.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index c02a1bc49..996bf5c29 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -387,7 +387,7 @@ def _deserializeTrackedObjects(self, serialized_objects): obj.asset_scale = obj_data.get('asset_scale') obj.vectors = [] # Empty list - tracked objects from MQTT don't have detection vectors obj.boundingBoxPixels = None # Will use camera_bounds from obj_data if available - + if 'first_seen' in obj_data: obj.when = get_epoch_time(obj_data.get('first_seen')) obj.first_seen = obj.when @@ -402,11 +402,11 @@ def _deserializeTrackedObjects(self, serialized_objects): 'category': obj.category, 'confidence': obj.confidence, } - + # Add center_of_mass if available if 'center_of_mass' in obj_data: obj.info['center_of_mass'] = obj_data['center_of_mass'] - + # Add camera_bounds if available if 'camera_bounds' in obj_data: obj.info['camera_bounds'] = obj_data['camera_bounds'] diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 220a450b5..e401dbebe 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -461,14 +461,14 @@ def handleSceneDataMessage(self, client, userdata, message): if self.disable_tracker: # Get tracked objects that Analytics will use analytics_objects = scene.getTrackedObjects(detection_type) - + # Prepare message data for publishing msg_when = get_epoch_time(jdata.get('timestamp')) - + # Publish detections using the tracked objects self.publishDetections(scene, analytics_objects, msg_when, detection_type, jdata, None) self.publishEvents(scene, jdata.get('timestamp')) - + return def _handleChildSceneObject(self, sender_id, jdata, detection_type, msg_when): From 317fffef10e5da974c2e72b1c082a18743f83ad3 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Wed, 17 Dec 2025 17:37:48 +0100 Subject: [PATCH 18/33] Get frame rate from mqtt --- controller/src/controller/scene_controller.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index e401dbebe..a36959e9a 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -175,8 +175,13 @@ def publishRegulatedDetections(self, scene_obj, msg_objects, otype, jdata, camer } scene = self.regulate_cache[scene_uid] scene['objects'][otype] = jdata['objects'] + + # Store the incoming rate from MQTT message or camera if camera_id is not None: scene['rate'][camera_id] = jdata.get('rate', None) + elif self.disable_tracker and 'rate' in jdata: + # When tracker is disabled, use the rate from the incoming MQTT scene data + scene['rate']['mqtt_scene'] = jdata['rate'] now = get_epoch_time() if self.shouldPublish(scene['last'], now, 1/scene_obj.regulated_rate): From 4d131d656afd413e6228db1d35f809de04297822 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Thu, 18 Dec 2025 10:48:33 +0100 Subject: [PATCH 19/33] Don't subscribe to camera and sensor topics when --tracker-disabled --- controller/src/controller/scene_controller.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index a36959e9a..41e95938f 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -363,6 +363,10 @@ def handleSensorMessage(self, client, userdata, message): return def handleMovingObjectMessage(self, client, userdata, message): + # When tracker is disabled, we don't process camera messages + if self.disable_tracker: + return + topic = PubSub.parseTopic(message.topic) jdata = orjson.loads(message.payload.decode('utf-8')) @@ -636,12 +640,14 @@ def updateSubscriptions(self): self.scenes = self.cache_manager.allScenes() for scene in self.scenes: - for camera in scene.cameras: - need_subscribe.add((PubSub.formatTopic(PubSub.DATA_CAMERA, camera_id=camera), - self.handleMovingObjectMessage)) - for sensor in scene.sensors: - need_subscribe.add((PubSub.formatTopic(PubSub.DATA_SENSOR, sensor_id=sensor), - self.handleSensorMessage)) + # Only subscribe to camera and sensor topics when tracker is enabled + if not self.disable_tracker: + for camera in scene.cameras: + need_subscribe.add((PubSub.formatTopic(PubSub.DATA_CAMERA, camera_id=camera), + self.handleMovingObjectMessage)) + for sensor in scene.sensors: + need_subscribe.add((PubSub.formatTopic(PubSub.DATA_SENSOR, sensor_id=sensor), + self.handleSensorMessage)) # Subscribe to scene data (tracked objects) for Analytics to consume # This reuses the existing DATA_SCENE topic that tracker already publishes to From 01e4e6e3912ffabc351f810da35977ab67df6813 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Thu, 18 Dec 2025 12:16:33 +0100 Subject: [PATCH 20/33] Subscribe to scene/data only when tracke disabled --- controller/src/controller/scene_controller.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 41e95938f..cd523e1f7 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -370,7 +370,6 @@ def handleMovingObjectMessage(self, client, userdata, message): topic = PubSub.parseTopic(message.topic) jdata = orjson.loads(message.payload.decode('utf-8')) - metric_attributes = { "topic": message.topic, "camera": jdata.get("id", "unknown"), @@ -651,9 +650,9 @@ def updateSubscriptions(self): # Subscribe to scene data (tracked objects) for Analytics to consume # This reuses the existing DATA_SCENE topic that tracker already publishes to - need_subscribe.add((PubSub.formatTopic(PubSub.DATA_SCENE, - scene_id=scene.uid, thing_type="+"), - self.handleSceneDataMessage)) + if self.disable_tracker: + need_subscribe.add((PubSub.formatTopic(PubSub.DATA_SCENE, scene_id=scene.uid, thing_type="+"), + self.handleSceneDataMessage)) if hasattr(scene, 'children'): child_scenes = self.cache_manager.data_source.getChildScenes(scene.uid) From 6a8b2582f963ef00a499c96231929052c0bc07f3 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Thu, 18 Dec 2025 12:44:00 +0100 Subject: [PATCH 21/33] Delete whitespace --- controller/src/controller/scene_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index cd523e1f7..1701a2190 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -175,7 +175,7 @@ def publishRegulatedDetections(self, scene_obj, msg_objects, otype, jdata, camer } scene = self.regulate_cache[scene_uid] scene['objects'][otype] = jdata['objects'] - + # Store the incoming rate from MQTT message or camera if camera_id is not None: scene['rate'][camera_id] = jdata.get('rate', None) @@ -366,7 +366,7 @@ def handleMovingObjectMessage(self, client, userdata, message): # When tracker is disabled, we don't process camera messages if self.disable_tracker: return - + topic = PubSub.parseTopic(message.topic) jdata = orjson.loads(message.payload.decode('utf-8')) From 77b5b44dfa7b15a66acce6ceec9f3a13fd2534e9 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Thu, 18 Dec 2025 14:16:47 +0100 Subject: [PATCH 22/33] Disable publishing to data/scene when tracke is dissabled --- controller/src/controller/scene_controller.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 1701a2190..000601b7e 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -130,7 +130,10 @@ def publishDetections(self, scene, objects, ts, otype, jdata, camera_id): "scene": scene.name } metrics.record_object_count(len(objects), metric_attributes) - self.publishSceneDetections(scene, objects, otype, jdata) + # Only publish to DATA_SCENE topic when tracker is enabled + # When tracker is disabled, we consume from DATA_SCENE instead of publishing to it + if not self.disable_tracker: + self.publishSceneDetections(scene, objects, otype, jdata) self.publishRegulatedDetections(scene, objects, otype, jdata, camera_id) self.publishRegionDetections(scene, objects, otype, jdata) return From 4c0e0fca129ccf2431b2a8a1784683f1888e2817 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Thu, 18 Dec 2025 14:26:53 +0100 Subject: [PATCH 23/33] Add logging --- controller/src/controller/scene_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 000601b7e..ef7d9609e 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -456,6 +456,7 @@ def handleSceneDataMessage(self, client, userdata, message): scene_id = topic['scene_id'] detection_type = topic['thing_type'] + log.debug(f"Received scene data message: scene={scene_id}, type={detection_type}, objects={len(jdata.get('objects', []))}") scene = self.cache_manager.sceneWithID(scene_id) if scene is None: @@ -472,6 +473,7 @@ def handleSceneDataMessage(self, client, userdata, message): if self.disable_tracker: # Get tracked objects that Analytics will use analytics_objects = scene.getTrackedObjects(detection_type) + log.debug(f"Publishing analytics: scene={scene_id}, type={detection_type}, objects={len(analytics_objects)}") # Prepare message data for publishing msg_when = get_epoch_time(jdata.get('timestamp')) From 5b69ee2843a2871697bfdcbbbc64c22d79cb1eb2 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Fri, 19 Dec 2025 14:10:06 +0100 Subject: [PATCH 24/33] WIP on mqtt messeges --- .../src/controller/detections_builder.py | 47 +++++++++++-------- controller/src/controller/scene.py | 45 +++++++++++++++++- controller/src/controller/scene_controller.py | 12 ++++- 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index d200e5195..ebfd2fd86 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -93,25 +93,32 @@ def prepareObjDict(scene, obj, update_visibility): def computeCameraBounds(scene, aobj, obj_dict): camera_bounds = {} - for cameraID in obj_dict['visibility']: - bounds = None - if aobj and len(aobj.vectors) > 0 and hasattr(aobj.vectors[0].camera, 'cameraID') \ - and cameraID == aobj.vectors[0].camera.cameraID: - bounds = getattr(aobj, 'boundingBoxPixels', None) - elif scene: - camera = scene.cameraWithID(cameraID) - if camera is not None and 'bb_meters' in obj_dict: - obj_translation = None - obj_size = None - if aobj: - obj_translation = aobj.sceneLoc - obj_size = aobj.bbMeters.size - else: - obj_translation = Point(obj_dict['translation']) - obj_size = Size(obj_dict['bb_meters']['width'], obj_dict['bb_meters']['height']) - bounds = camera.pose.projectEstimatedBoundsToCameraPixels(obj_translation, - obj_size) - if bounds: - camera_bounds[cameraID] = bounds.asDict + + # First, check if camera_bounds already exist in obj_dict (from MQTT data) + if 'camera_bounds' in obj_dict and obj_dict['camera_bounds']: + camera_bounds = obj_dict['camera_bounds'] + else: + # Compute camera_bounds if not provided + for cameraID in obj_dict['visibility']: + bounds = None + if aobj and len(aobj.vectors) > 0 and hasattr(aobj.vectors[0].camera, 'cameraID') \ + and cameraID == aobj.vectors[0].camera.cameraID: + bounds = getattr(aobj, 'boundingBoxPixels', None) + elif scene: + camera = scene.cameraWithID(cameraID) + if camera is not None and 'bb_meters' in obj_dict: + obj_translation = None + obj_size = None + if aobj: + obj_translation = aobj.sceneLoc + obj_size = aobj.bbMeters.size + else: + obj_translation = Point(obj_dict['translation']) + obj_size = Size(obj_dict['bb_meters']['width'], obj_dict['bb_meters']['height']) + bounds = camera.pose.projectEstimatedBoundsToCameraPixels(obj_translation, + obj_size) + if bounds: + camera_bounds[cameraID] = bounds.asDict + obj_dict['camera_bounds'] = camera_bounds return diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 996bf5c29..f0bd9264f 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -358,6 +358,42 @@ def getTrackedObjects(self, detection_type): return [] + def _estimateCameraBoundsFromCenterOfMass(self, center_of_mass, visibility): + """ + Estimate camera_bounds from center_of_mass when not provided in MQTT data. + This provides a reasonable fallback for tracked objects without explicit bounds. + + Args: + center_of_mass: Dict with x, y, width, height + visibility: List of camera IDs + + Returns: + Dict mapping camera IDs to bounding boxes + """ + camera_bounds = {} + if not center_of_mass or not visibility: + return camera_bounds + + # Extract center of mass position and size + com_x = center_of_mass.get('x', 0) + com_y = center_of_mass.get('y', 0) + bbox_w = center_of_mass.get('width', 50) + bbox_h = center_of_mass.get('height', 85) + + # Compute bounding box for each visible camera + for cam_id in visibility: + # Center the bbox around center_of_mass + x = max(0, int(com_x - bbox_w / 2)) + y = max(0, int(com_y - bbox_h / 2)) + camera_bounds[cam_id] = { + 'x': x, + 'y': y, + 'width': int(bbox_w), + 'height': int(bbox_h) + } + + return camera_bounds + def _deserializeTrackedObjects(self, serialized_objects): """ Convert serialized tracked objects to a format usable by Analytics. @@ -407,9 +443,14 @@ def _deserializeTrackedObjects(self, serialized_objects): if 'center_of_mass' in obj_data: obj.info['center_of_mass'] = obj_data['center_of_mass'] - # Add camera_bounds if available - if 'camera_bounds' in obj_data: + # Add camera_bounds if available, or compute from center_of_mass + if 'camera_bounds' in obj_data and obj_data['camera_bounds']: obj.info['camera_bounds'] = obj_data['camera_bounds'] + elif 'center_of_mass' in obj_data and obj.visibility: + # Fallback: estimate camera_bounds from center_of_mass + com = obj_data['center_of_mass'] + obj.info['camera_bounds'] = self._estimateCameraBoundsFromCenterOfMass(com, obj.visibility) + log.debug(f"Estimated camera_bounds from center_of_mass for object {obj.gid}") # Chain data for regions, sensors, and published locations obj.chain_data = SimpleNamespace() diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index ef7d9609e..27902a334 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -183,8 +183,16 @@ def publishRegulatedDetections(self, scene_obj, msg_objects, otype, jdata, camer if camera_id is not None: scene['rate'][camera_id] = jdata.get('rate', None) elif self.disable_tracker and 'rate' in jdata: - # When tracker is disabled, use the rate from the incoming MQTT scene data - scene['rate']['mqtt_scene'] = jdata['rate'] + # When tracker is disabled, distribute the scene rate to all visible cameras + # Extract unique camera IDs from all objects' visibility lists + camera_ids = set() + for obj in jdata.get('objects', []): + camera_ids.update(obj.get('visibility', [])) + + # Store the same rate for each camera that has visibility + scene_rate = jdata['rate'] + for cam_id in camera_ids: + scene['rate'][cam_id] = scene_rate now = get_epoch_time() if self.shouldPublish(scene['last'], now, 1/scene_obj.regulated_rate): From 106a768eccae4b8ace413bbe5495b276b021effb Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Fri, 19 Dec 2025 15:06:27 +0100 Subject: [PATCH 25/33] WIP debug --- controller/src/controller/scene_controller.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 27902a334..a94f0c600 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -177,7 +177,9 @@ def publishRegulatedDetections(self, scene_obj, msg_objects, otype, jdata, camer 'last': None } scene = self.regulate_cache[scene_uid] - scene['objects'][otype] = jdata['objects'] + + # Build the objects list from msg_objects (handles both tracker enabled and disabled) + scene['objects'][otype] = buildDetectionsList(msg_objects, scene_obj, self.visibility_topic == 'unregulated') # Store the incoming rate from MQTT message or camera if camera_id is not None: @@ -213,6 +215,7 @@ def publishRegulatedDetections(self, scene_obj, msg_objects, otype, jdata, camer if aobj is not None: computeCameraBounds(scene_obj, aobj, obj) objects.append(obj) + log.debug(f"Publishing regulated: scene={scene_uid}, objects_count={len(objects)}, types={list(scene['objects'].keys())}") new_jdata = { 'timestamp': jdata['timestamp'], 'objects': objects, @@ -481,7 +484,11 @@ def handleSceneDataMessage(self, client, userdata, message): if self.disable_tracker: # Get tracked objects that Analytics will use analytics_objects = scene.getTrackedObjects(detection_type) - log.debug(f"Publishing analytics: scene={scene_id}, type={detection_type}, objects={len(analytics_objects)}") + log.info(f"Tracker disabled - received objects: scene={scene_id}, type={detection_type}, count={len(analytics_objects)}") + + # Update visibility based on scene's camera configuration + # This ensures visibility matches the actual scene setup, not just the MQTT data + scene._updateVisible(analytics_objects) # Prepare message data for publishing msg_when = get_epoch_time(jdata.get('timestamp')) From 3e13572633fc071c20067bc2467e2a5f113b6abd Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Fri, 19 Dec 2025 15:39:09 +0100 Subject: [PATCH 26/33] remove field --- controller/src/controller/scene.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index f0bd9264f..4809589d7 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -412,7 +412,18 @@ def _deserializeTrackedObjects(self, serialized_objects): obj = SimpleNamespace() obj.gid = obj_data.get('id') obj.category = obj_data.get('type', obj_data.get('category')) - obj.sceneLoc = Point(obj_data.get('translation', [0, 0, 0])) + + # Handle translation - if null/invalid, we can't determine 3D position without calibration + # For now, use [0, 0, 0] as placeholder - objects without valid translation won't be plottable + translation = obj_data.get('translation', [0, 0, 0]) + if translation and None not in translation and all(isinstance(x, (int, float)) for x in translation): + obj.sceneLoc = Point(translation) + else: + # Invalid translation - object doesn't have 3D world coordinates + # This is expected in tracker-disabled mode without camera calibration + obj.sceneLoc = Point([0, 0, 0]) + log.debug(f"Object {obj_data.get('id')} has invalid translation: {translation}") + obj.velocity = Point(obj_data.get('velocity', [0, 0, 0])) if obj_data.get('velocity') else None obj.size = obj_data.get('size') obj.confidence = obj_data.get('confidence') @@ -420,7 +431,6 @@ def _deserializeTrackedObjects(self, serialized_objects): obj.rotation = obj_data.get('rotation') obj.reidVector = obj_data.get('reid') obj.similarity = obj_data.get('similarity') - obj.asset_scale = obj_data.get('asset_scale') obj.vectors = [] # Empty list - tracked objects from MQTT don't have detection vectors obj.boundingBoxPixels = None # Will use camera_bounds from obj_data if available From 5f10e075a494f434f2f3a58fa99618a91aff2f8f Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Fri, 19 Dec 2025 15:40:08 +0100 Subject: [PATCH 27/33] WIP --- controller/src/controller/scene.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 4809589d7..6d6808d0b 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -412,18 +412,7 @@ def _deserializeTrackedObjects(self, serialized_objects): obj = SimpleNamespace() obj.gid = obj_data.get('id') obj.category = obj_data.get('type', obj_data.get('category')) - - # Handle translation - if null/invalid, we can't determine 3D position without calibration - # For now, use [0, 0, 0] as placeholder - objects without valid translation won't be plottable - translation = obj_data.get('translation', [0, 0, 0]) - if translation and None not in translation and all(isinstance(x, (int, float)) for x in translation): - obj.sceneLoc = Point(translation) - else: - # Invalid translation - object doesn't have 3D world coordinates - # This is expected in tracker-disabled mode without camera calibration - obj.sceneLoc = Point([0, 0, 0]) - log.debug(f"Object {obj_data.get('id')} has invalid translation: {translation}") - + obj.sceneLoc = Point(obj_data.get('translation', [0, 0, 0])) obj.velocity = Point(obj_data.get('velocity', [0, 0, 0])) if obj_data.get('velocity') else None obj.size = obj_data.get('size') obj.confidence = obj_data.get('confidence') From 25fc819e589632a6abf433d12aa66721ef8fdf4c Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Fri, 19 Dec 2025 15:57:49 +0100 Subject: [PATCH 28/33] Remove whitespace --- controller/src/controller/scene_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index a94f0c600..aa1bdf1ab 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -190,7 +190,7 @@ def publishRegulatedDetections(self, scene_obj, msg_objects, otype, jdata, camer camera_ids = set() for obj in jdata.get('objects', []): camera_ids.update(obj.get('visibility', [])) - + # Store the same rate for each camera that has visibility scene_rate = jdata['rate'] for cam_id in camera_ids: From 4cba62e3965ded2b40d3eeba997cf7fceb392e20 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Mon, 29 Dec 2025 12:22:12 +0100 Subject: [PATCH 29/33] Change order --- .../src/controller/detections_builder.py | 58 +++++++++---------- controller/src/controller/scene.py | 8 +-- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index ebfd2fd86..24e4522f6 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -46,10 +46,6 @@ def prepareObjDict(scene, obj, update_visibility): 'velocity': velocity.asCartesianVector }) - # Add frame count for analytics - if hasattr(aobj, 'frameCount'): - obj_dict['frame_count'] = aobj.frameCount - rotation = aobj.rotation if rotation is not None: obj_dict['rotation'] = rotation @@ -71,6 +67,8 @@ def prepareObjDict(scene, obj, update_visibility): obj_dict['visibility'] = aobj.visibility if update_visibility: computeCameraBounds(scene, aobj, obj_dict) + elif hasattr(aobj, '_camera_bounds') and aobj._camera_bounds: + obj_dict['camera_bounds'] = aobj._camera_bounds chain_data = aobj.chain_data if len(chain_data.regions): @@ -83,6 +81,11 @@ def prepareObjDict(scene, obj, update_visibility): obj_dict['similarity'] = aobj.similarity if hasattr(aobj, 'first_seen'): obj_dict['first_seen'] = get_iso_time(aobj.first_seen) + + if not update_visibility and hasattr(aobj, '_camera_bounds') and aobj._camera_bounds: + if 'camera_bounds' not in obj_dict: + obj_dict['camera_bounds'] = aobj._camera_bounds + if isinstance(obj, TripwireEvent): obj_dict['direction'] = obj.direction if hasattr(aobj, 'asset_scale'): @@ -93,32 +96,25 @@ def prepareObjDict(scene, obj, update_visibility): def computeCameraBounds(scene, aobj, obj_dict): camera_bounds = {} - - # First, check if camera_bounds already exist in obj_dict (from MQTT data) - if 'camera_bounds' in obj_dict and obj_dict['camera_bounds']: - camera_bounds = obj_dict['camera_bounds'] - else: - # Compute camera_bounds if not provided - for cameraID in obj_dict['visibility']: - bounds = None - if aobj and len(aobj.vectors) > 0 and hasattr(aobj.vectors[0].camera, 'cameraID') \ - and cameraID == aobj.vectors[0].camera.cameraID: - bounds = getattr(aobj, 'boundingBoxPixels', None) - elif scene: - camera = scene.cameraWithID(cameraID) - if camera is not None and 'bb_meters' in obj_dict: - obj_translation = None - obj_size = None - if aobj: - obj_translation = aobj.sceneLoc - obj_size = aobj.bbMeters.size - else: - obj_translation = Point(obj_dict['translation']) - obj_size = Size(obj_dict['bb_meters']['width'], obj_dict['bb_meters']['height']) - bounds = camera.pose.projectEstimatedBoundsToCameraPixels(obj_translation, - obj_size) - if bounds: - camera_bounds[cameraID] = bounds.asDict - + for cameraID in obj_dict['visibility']: + bounds = None + if aobj and len(aobj.vectors) > 0 and hasattr(aobj.vectors[0].camera, 'cameraID') \ + and cameraID == aobj.vectors[0].camera.cameraID: + bounds = getattr(aobj, 'boundingBoxPixels', None) + elif scene: + camera = scene.cameraWithID(cameraID) + if camera is not None and 'bb_meters' in obj_dict: + obj_translation = None + obj_size = None + if aobj: + obj_translation = aobj.sceneLoc + obj_size = aobj.bbMeters.size + else: + obj_translation = Point(obj_dict['translation']) + obj_size = Size(obj_dict['bb_meters']['width'], obj_dict['bb_meters']['height']) + bounds = camera.pose.projectEstimatedBoundsToCameraPixels(obj_translation, + obj_size) + if bounds: + camera_bounds[cameraID] = bounds.asDict obj_dict['camera_bounds'] = camera_bounds return diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 6d6808d0b..5fa3ade41 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -442,14 +442,14 @@ def _deserializeTrackedObjects(self, serialized_objects): if 'center_of_mass' in obj_data: obj.info['center_of_mass'] = obj_data['center_of_mass'] - # Add camera_bounds if available, or compute from center_of_mass if 'camera_bounds' in obj_data and obj_data['camera_bounds']: - obj.info['camera_bounds'] = obj_data['camera_bounds'] + obj._camera_bounds = obj_data['camera_bounds'] elif 'center_of_mass' in obj_data and obj.visibility: - # Fallback: estimate camera_bounds from center_of_mass com = obj_data['center_of_mass'] - obj.info['camera_bounds'] = self._estimateCameraBoundsFromCenterOfMass(com, obj.visibility) + obj._camera_bounds = self._estimateCameraBoundsFromCenterOfMass(com, obj.visibility) log.debug(f"Estimated camera_bounds from center_of_mass for object {obj.gid}") + else: + obj._camera_bounds = None # Chain data for regions, sensors, and published locations obj.chain_data = SimpleNamespace() From 73bc1d9601972873fb51273571bcc3b318e2abd7 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Mon, 29 Dec 2025 12:50:11 +0100 Subject: [PATCH 30/33] WIP --- controller/src/controller/scene.py | 9 ++++----- controller/src/controller/scene_controller.py | 7 +------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 5fa3ade41..34e420a61 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -444,10 +444,6 @@ def _deserializeTrackedObjects(self, serialized_objects): if 'camera_bounds' in obj_data and obj_data['camera_bounds']: obj._camera_bounds = obj_data['camera_bounds'] - elif 'center_of_mass' in obj_data and obj.visibility: - com = obj_data['center_of_mass'] - obj._camera_bounds = self._estimateCameraBoundsFromCenterOfMass(com, obj.visibility) - log.debug(f"Estimated camera_bounds from center_of_mass for object {obj.gid}") else: obj._camera_bounds = None @@ -466,7 +462,10 @@ def _deserializeTrackedObjects(self, serialized_objects): def _updateEvents(self, detectionType, now): self.events = {} now_str = get_iso_time(now) - curObjects = self.getTrackedObjects(detectionType) + if self.disable_tracker: + curObjects = self.getTrackedObjects(detectionType) + else: + curObjects = self.tracker.currentObjects(detectionType) if self.tracker else [] for obj in curObjects: obj.chain_data.publishedLocations.insert(0, obj.sceneLoc) diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index aa1bdf1ab..236d56c3c 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -482,15 +482,10 @@ def handleSceneDataMessage(self, client, userdata, message): # When tracker is disabled, we need to publish analytics based on tracked objects from MQTT if self.disable_tracker: - # Get tracked objects that Analytics will use analytics_objects = scene.getTrackedObjects(detection_type) - log.info(f"Tracker disabled - received objects: scene={scene_id}, type={detection_type}, count={len(analytics_objects)}") - - # Update visibility based on scene's camera configuration - # This ensures visibility matches the actual scene setup, not just the MQTT data + log.debug(f"Tracker disabled - received objects: scene={scene_id}, type={detection_type}, count={len(analytics_objects)}") scene._updateVisible(analytics_objects) - # Prepare message data for publishing msg_when = get_epoch_time(jdata.get('timestamp')) # Publish detections using the tracked objects From 2fd07c59ffb757ee444a89bf566f813c2782da3f Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Tue, 30 Dec 2025 12:55:56 +0100 Subject: [PATCH 31/33] Fix analytics mode: preserve camera_bounds and ref_camera_frame_rate from MQTT - Add _normalize_camera_bounds() to convert int->float for consistency - Extract ref_camera_frame_rate before early returns in analytics mode - Preserve camera_bounds from MQTT in _deserializeTrackedObjects() - Add safety check in computeCameraBounds() for missing visibility key - Add field ordering (similarity, first_seen before camera_bounds) --- .../src/controller/detections_builder.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index 24e4522f6..d2d774de4 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: (C) 2024 - 2025 Intel Corporation # SPDX-License-Identifier: Apache-2.0 +import logging import numpy as np from controller.scene import TripwireEvent @@ -8,6 +9,27 @@ from scene_common.geometry import DEFAULTZ, Point, Size from scene_common.timestamp import get_iso_time +log = logging.getLogger(__name__) + + +def _normalize_camera_bounds(bounds): + """Convert camera_bounds numeric values to floats for consistency with + defaults produced by the tracker pipeline (which uses floats).""" + if not isinstance(bounds, dict): + return bounds + out = {} + for cam_id, b in bounds.items(): + try: + out[cam_id] = { + 'x': float(b.get('x')) if b.get('x') is not None else None, + 'y': float(b.get('y')) if b.get('y') is not None else None, + 'width': float(b.get('width')) if b.get('width') is not None else None, + 'height': float(b.get('height')) if b.get('height') is not None else None, + } + except (TypeError, ValueError): + out[cam_id] = b + return out + def buildDetectionsDict(objects, scene): result_dict = {} @@ -65,10 +87,30 @@ def prepareObjDict(scene, obj, update_visibility): if hasattr(aobj, 'visibility'): obj_dict['visibility'] = aobj.visibility +<<<<<<< HEAD if update_visibility: computeCameraBounds(scene, aobj, obj_dict) elif hasattr(aobj, '_camera_bounds') and aobj._camera_bounds: obj_dict['camera_bounds'] = aobj._camera_bounds +======= + + # Add similarity and first_seen before camera_bounds to maintain consistent field ordering + if hasattr(aobj, 'similarity'): + obj_dict['similarity'] = aobj.similarity + if hasattr(aobj, 'first_seen'): + obj_dict['first_seen'] = get_iso_time(aobj.first_seen) + + # Handle camera_bounds + if hasattr(aobj, 'visibility'): + # Only compute camera_bounds if tracker is enabled and update_visibility is requested + # In analytics-only mode (tracker disabled), camera_bounds comes from MQTT + if update_visibility and not scene.disable_tracker: + computeCameraBounds(scene, aobj, obj_dict) + elif scene.disable_tracker and hasattr(aobj, '_camera_bounds'): + # Use camera_bounds from MQTT (analytics-only mode) + if aobj._camera_bounds: + obj_dict['camera_bounds'] = aobj._camera_bounds +>>>>>>> f967aeb9 (Fix analytics mode: preserve camera_bounds and ref_camera_frame_rate from MQTT) chain_data = aobj.chain_data if len(chain_data.regions): @@ -95,7 +137,16 @@ def prepareObjDict(scene, obj, update_visibility): return obj_dict def computeCameraBounds(scene, aobj, obj_dict): +<<<<<<< HEAD camera_bounds = {} +======= + # If incoming obj_dict already contains non-empty camera_bounds (e.g., from MQTT), + # avoid overwriting them with an empty dict computed here. + camera_bounds = obj_dict.get('camera_bounds', {}) or {} + # Return early if no visibility info - nothing to compute + if 'visibility' not in obj_dict: + return +>>>>>>> f967aeb9 (Fix analytics mode: preserve camera_bounds and ref_camera_frame_rate from MQTT) for cameraID in obj_dict['visibility']: bounds = None if aobj and len(aobj.vectors) > 0 and hasattr(aobj.vectors[0].camera, 'cameraID') \ From 2bab1a735815d4d823bff0e4e797c1aca479ed79 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Tue, 30 Dec 2025 12:58:28 +0100 Subject: [PATCH 32/33] Remove _normalize_camera_bounds --- .../src/controller/detections_builder.py | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index d2d774de4..0a5fb4d14 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -11,26 +11,6 @@ log = logging.getLogger(__name__) - -def _normalize_camera_bounds(bounds): - """Convert camera_bounds numeric values to floats for consistency with - defaults produced by the tracker pipeline (which uses floats).""" - if not isinstance(bounds, dict): - return bounds - out = {} - for cam_id, b in bounds.items(): - try: - out[cam_id] = { - 'x': float(b.get('x')) if b.get('x') is not None else None, - 'y': float(b.get('y')) if b.get('y') is not None else None, - 'width': float(b.get('width')) if b.get('width') is not None else None, - 'height': float(b.get('height')) if b.get('height') is not None else None, - } - except (TypeError, ValueError): - out[cam_id] = b - return out - - def buildDetectionsDict(objects, scene): result_dict = {} for obj in objects: @@ -87,12 +67,6 @@ def prepareObjDict(scene, obj, update_visibility): if hasattr(aobj, 'visibility'): obj_dict['visibility'] = aobj.visibility -<<<<<<< HEAD - if update_visibility: - computeCameraBounds(scene, aobj, obj_dict) - elif hasattr(aobj, '_camera_bounds') and aobj._camera_bounds: - obj_dict['camera_bounds'] = aobj._camera_bounds -======= # Add similarity and first_seen before camera_bounds to maintain consistent field ordering if hasattr(aobj, 'similarity'): @@ -110,7 +84,6 @@ def prepareObjDict(scene, obj, update_visibility): # Use camera_bounds from MQTT (analytics-only mode) if aobj._camera_bounds: obj_dict['camera_bounds'] = aobj._camera_bounds ->>>>>>> f967aeb9 (Fix analytics mode: preserve camera_bounds and ref_camera_frame_rate from MQTT) chain_data = aobj.chain_data if len(chain_data.regions): @@ -137,16 +110,12 @@ def prepareObjDict(scene, obj, update_visibility): return obj_dict def computeCameraBounds(scene, aobj, obj_dict): -<<<<<<< HEAD - camera_bounds = {} -======= # If incoming obj_dict already contains non-empty camera_bounds (e.g., from MQTT), # avoid overwriting them with an empty dict computed here. camera_bounds = obj_dict.get('camera_bounds', {}) or {} # Return early if no visibility info - nothing to compute if 'visibility' not in obj_dict: return ->>>>>>> f967aeb9 (Fix analytics mode: preserve camera_bounds and ref_camera_frame_rate from MQTT) for cameraID in obj_dict['visibility']: bounds = None if aobj and len(aobj.vectors) > 0 and hasattr(aobj.vectors[0].camera, 'cameraID') \ From e650bd3a667d4be6e2d71ec2fd307fe4a9afd8f2 Mon Sep 17 00:00:00 2001 From: "Yermolenko, Dmytro" Date: Wed, 31 Dec 2025 12:21:27 +0100 Subject: [PATCH 33/33] CleanUp --- .../src/controller/detections_builder.py | 10 ---- controller/src/controller/scene.py | 58 +++++-------------- controller/src/controller/scene_controller.py | 1 - 3 files changed, 15 insertions(+), 54 deletions(-) diff --git a/controller/src/controller/detections_builder.py b/controller/src/controller/detections_builder.py index 0a5fb4d14..585b6c1c4 100644 --- a/controller/src/controller/detections_builder.py +++ b/controller/src/controller/detections_builder.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: (C) 2024 - 2025 Intel Corporation # SPDX-License-Identifier: Apache-2.0 -import logging import numpy as np from controller.scene import TripwireEvent @@ -9,7 +8,6 @@ from scene_common.geometry import DEFAULTZ, Point, Size from scene_common.timestamp import get_iso_time -log = logging.getLogger(__name__) def buildDetectionsDict(objects, scene): result_dict = {} @@ -68,20 +66,15 @@ def prepareObjDict(scene, obj, update_visibility): if hasattr(aobj, 'visibility'): obj_dict['visibility'] = aobj.visibility - # Add similarity and first_seen before camera_bounds to maintain consistent field ordering if hasattr(aobj, 'similarity'): obj_dict['similarity'] = aobj.similarity if hasattr(aobj, 'first_seen'): obj_dict['first_seen'] = get_iso_time(aobj.first_seen) - # Handle camera_bounds if hasattr(aobj, 'visibility'): - # Only compute camera_bounds if tracker is enabled and update_visibility is requested - # In analytics-only mode (tracker disabled), camera_bounds comes from MQTT if update_visibility and not scene.disable_tracker: computeCameraBounds(scene, aobj, obj_dict) elif scene.disable_tracker and hasattr(aobj, '_camera_bounds'): - # Use camera_bounds from MQTT (analytics-only mode) if aobj._camera_bounds: obj_dict['camera_bounds'] = aobj._camera_bounds @@ -110,10 +103,7 @@ def prepareObjDict(scene, obj, update_visibility): return obj_dict def computeCameraBounds(scene, aobj, obj_dict): - # If incoming obj_dict already contains non-empty camera_bounds (e.g., from MQTT), - # avoid overwriting them with an empty dict computed here. camera_bounds = obj_dict.get('camera_bounds', {}) or {} - # Return early if no visibility info - nothing to compute if 'visibility' not in obj_dict: return for cameraID in obj_dict['visibility']: diff --git a/controller/src/controller/scene.py b/controller/src/controller/scene.py index 34e420a61..850edec83 100644 --- a/controller/src/controller/scene.py +++ b/controller/src/controller/scene.py @@ -145,6 +145,12 @@ def _createMovingObjectsForDetection(self, detectionType, detections, when, came return objects def processCameraData(self, jdata, when=None, ignoreTimeFlag=False): + + # Skip processing if tracker is disabled - data should come from separate Tracker service via MQTT + if self.disable_tracker: + log.debug(f"Tracker disabled, skipping camera data processing for camera {camera_id}") + return True + camera_id = jdata['id'] camera = None @@ -166,11 +172,6 @@ def processCameraData(self, jdata, when=None, ignoreTimeFlag=False): log.info("DISCARDING: camera has no pose") return True - # Skip processing if tracker is disabled - data should come from separate Tracker service via MQTT - if self.disable_tracker: - log.debug(f"Tracker disabled, skipping camera data processing for camera {camera_id}") - return True - for detection_type, detections in jdata['objects'].items(): if "intrinsics" not in jdata: self._convertPixelBoundingBoxesToMeters(detections, camera.pose.intrinsics.intrinsics, camera.pose.intrinsics.distortion) @@ -229,6 +230,7 @@ def processSceneData(self, jdata, child, cameraPose, detectionType, when=None): new = jdata['objects'] + # Update ref_camera_frame_rate before early return (needed for analytics mode) if 'frame_rate' in jdata: self.ref_camera_frame_rate = min(jdata['frame_rate'], self.ref_camera_frame_rate) if self.ref_camera_frame_rate is not None else jdata["frame_rate"] @@ -358,42 +360,6 @@ def getTrackedObjects(self, detection_type): return [] - def _estimateCameraBoundsFromCenterOfMass(self, center_of_mass, visibility): - """ - Estimate camera_bounds from center_of_mass when not provided in MQTT data. - This provides a reasonable fallback for tracked objects without explicit bounds. - - Args: - center_of_mass: Dict with x, y, width, height - visibility: List of camera IDs - - Returns: - Dict mapping camera IDs to bounding boxes - """ - camera_bounds = {} - if not center_of_mass or not visibility: - return camera_bounds - - # Extract center of mass position and size - com_x = center_of_mass.get('x', 0) - com_y = center_of_mass.get('y', 0) - bbox_w = center_of_mass.get('width', 50) - bbox_h = center_of_mass.get('height', 85) - - # Compute bounding box for each visible camera - for cam_id in visibility: - # Center the bbox around center_of_mass - x = max(0, int(com_x - bbox_w / 2)) - y = max(0, int(com_y - bbox_h / 2)) - camera_bounds[cam_id] = { - 'x': x, - 'y': y, - 'width': int(bbox_w), - 'height': int(bbox_h) - } - - return camera_bounds - def _deserializeTrackedObjects(self, serialized_objects): """ Convert serialized tracked objects to a format usable by Analytics. @@ -596,8 +562,11 @@ def _updateVisible(self, curObjects): @classmethod def deserialize(cls, data, disable_tracker=False): tracker_config = data.get('tracker_config', []) - scene = cls(data['name'], data.get('map', None), data.get('scale', None), - *tracker_config, disable_tracker) + scale_from_data = data.get('scale', None) + if scale_from_data is None and disable_tracker: + log.warning(f"Scene '{data.get('name')}': scale is None when deserializing in disable_tracker mode. Ensure scale is configured in the database or scene JSON file.") + scene = cls(data['name'], data.get('map', None), scale_from_data, + *tracker_config, disable_tracker=disable_tracker) scene.uid = data['uid'] scene.mesh_translation = data.get('mesh_translation', None) scene.mesh_rotation = data.get('mesh_rotation', None) @@ -609,6 +578,9 @@ def deserialize(cls, data, disable_tracker=False): scene.regulated_rate = data.get('regulated_rate', None) scene.external_update_rate = data.get('external_update_rate', None) scene.persist_attributes = data.get('persist_attributes', {}) + # Ensure scale is set from data even if __init__ didn't handle it correctly + if 'scale' in data: + scene.scale = data['scale'] if 'cameras' in data: scene.updateCameras(data['cameras']) if 'regions' in data: diff --git a/controller/src/controller/scene_controller.py b/controller/src/controller/scene_controller.py index 236d56c3c..d6d049ed0 100644 --- a/controller/src/controller/scene_controller.py +++ b/controller/src/controller/scene_controller.py @@ -42,7 +42,6 @@ def __init__(self, rewrite_bad_time, rewrite_all_time, max_lag, mqtt_broker, if disable_tracker: log.info("Tracker is DISABLED. Controller will run without tracker functionality.") - # Still load tracker config for analytics and other purposes pass if tracker_config_file is not None: