ITEP-23442: Include dwell time in region updates topic#1300
ITEP-23442: Include dwell time in region updates topic#1300saratpoluri wants to merge 1 commit intomainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds “live region dwell time” to region-scoped object outputs (region data + region events) so consumers can see how long an object has been inside a region at publish time.
Changes:
- Extend detections serialization to optionally include
regions.<region_id>.dwell(seconds sinceentered). - Update Scene Controller region/region-event publishing paths to request region dwell.
- Update documentation and unit tests to cover/describe the new dwell behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
controller/src/controller/detections_builder.py |
Adds optional region dwell computation during object serialization. |
controller/src/controller/scene_controller.py |
Enables dwell inclusion for region detections and region event payload object lists. |
tests/sscape_tests/scenescape/test_detections_builder.py |
Updates timestamp format expectations and adds a unit test for dwell inclusion. |
docs/user-guide/microservices/controller/data_formats.md |
Documents live region dwell field in region-scoped payloads. |
docs/user-guide/microservices/controller/_assets/scene-controller-api.yaml |
Updates AsyncAPI text to mention regions.<region_id>.dwell in region payloads. |
| | Field | Type | Description | | ||
| | ------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `timestamp` | string (ISO 8601 UTC) | Event timestamp | | ||
| | `scene_id` | string | Scene identifier (UUID) | | ||
| | `scene_name` | string | Scene name | | ||
| | `region_id` | string | Region identifier (UUID) | | ||
| | `region_name` | string | Region name | | ||
| | `counts` | object | Map of category to object count currently inside the region (e.g. `{"person": 2}`) | | ||
| | `objects` | array | Tracked objects currently inside the region. Each object includes live `regions.<region_name>.dwell` in addition to [Common Output Track Fields](#common-output-track-fields) | | ||
| | `entered` | array | Objects that entered the region during this cycle; each element is a bare track object and may include live `regions.<region_name>.dwell`. Empty when no entry occurred | | ||
| | `exited` | array | Objects that exited the region during this cycle; each element is `{"object": <track>, "dwell": <seconds>}`. Empty when no exit occurred | | ||
| | `metadata` | object | Region geometry: `title`, `uuid`, `points` (polygon vertices in metres), `area` (`"poly"`), `fromSensor` (boolean) | |
There was a problem hiding this comment.
The Region Event docs describe the live dwell field as regions.<region_name>.dwell, but the regions map is keyed by region UUID/ID (see example above where regions keys are UUIDs). This should refer to regions.<region_id>.dwell to match actual payload structure and avoid confusing consumers.
| | Field | Type | Description | | |
| | ------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |
| | `timestamp` | string (ISO 8601 UTC) | Event timestamp | | |
| | `scene_id` | string | Scene identifier (UUID) | | |
| | `scene_name` | string | Scene name | | |
| | `region_id` | string | Region identifier (UUID) | | |
| | `region_name` | string | Region name | | |
| | `counts` | object | Map of category to object count currently inside the region (e.g. `{"person": 2}`) | | |
| | `objects` | array | Tracked objects currently inside the region. Each object includes live `regions.<region_name>.dwell` in addition to [Common Output Track Fields](#common-output-track-fields) | | |
| | `entered` | array | Objects that entered the region during this cycle; each element is a bare track object and may include live `regions.<region_name>.dwell`. Empty when no entry occurred | | |
| | `exited` | array | Objects that exited the region during this cycle; each element is `{"object": <track>, "dwell": <seconds>}`. Empty when no exit occurred | | |
| | `metadata` | object | Region geometry: `title`, `uuid`, `points` (polygon vertices in metres), `area` (`"poly"`), `fromSensor` (boolean) | | |
| | Field | Type | Description | | |
| | ------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | |
| | `timestamp` | string (ISO 8601 UTC) | Event timestamp | | |
| | `scene_id` | string | Scene identifier (UUID) | | |
| | `scene_name` | string | Scene name | | |
| | `region_id` | string | Region identifier (UUID) | | |
| | `region_name` | string | Region name | | |
| | `counts` | object | Map of category to object count currently inside the region (e.g. `{"person": 2}`) | | |
| | `objects` | array | Tracked objects currently inside the region. Each object includes live `regions.<region_id>.dwell` in addition to [Common Output Track Fields](#common-output-track-fields) | | |
| | `entered` | array | Objects that entered the region during this cycle; each element is a bare track object and may include live `regions.<region_id>.dwell`. Empty when no entry occurred | | |
| | `exited` | array | Objects that exited the region during this cycle; each element is `{"object": <track>, "dwell": <seconds>}`. Empty when no exit occurred | | |
| | `metadata` | object | Region geometry: `title`, `uuid`, `points` (polygon vertices in metres), `area` (`"poly"`), `fromSensor` (boolean) | |
| def _build_region_output(regions, include_region_dwell, current_time): | ||
| serialized_regions = {} | ||
| for region_name, region_data in regions.items(): | ||
| serialized_region = dict(region_data) | ||
| if include_region_dwell and 'entered' in region_data: | ||
| entered = get_epoch_time(region_data['entered']) | ||
| serialized_region['dwell'] = current_time - entered |
There was a problem hiding this comment.
Computing dwell currently calls get_epoch_time() (which uses datetime.strptime) for every region on every serialized object. In region-scoped outputs this can become a hot path with many objects/regions per frame. Consider caching an epoch form of entered in chain_data.regions at entry time (or storing entered_epoch alongside the ISO string) so dwell can be computed without repeated string parsing.
| def _build_region_output(regions, include_region_dwell, current_time): | |
| serialized_regions = {} | |
| for region_name, region_data in regions.items(): | |
| serialized_region = dict(region_data) | |
| if include_region_dwell and 'entered' in region_data: | |
| entered = get_epoch_time(region_data['entered']) | |
| serialized_region['dwell'] = current_time - entered | |
| def _get_region_entered_epoch(region_data): | |
| entered_epoch = region_data.get('entered_epoch') | |
| if entered_epoch is None and 'entered' in region_data: | |
| entered_epoch = get_epoch_time(region_data['entered']) | |
| region_data['entered_epoch'] = entered_epoch | |
| return entered_epoch | |
| def _build_region_output(regions, include_region_dwell, current_time): | |
| serialized_regions = {} | |
| for region_name, region_data in regions.items(): | |
| serialized_region = dict(region_data) | |
| serialized_region.pop('entered_epoch', None) | |
| if include_region_dwell and 'entered' in region_data: | |
| entered_epoch = _get_region_entered_epoch(region_data) | |
| serialized_region['dwell'] = current_time - entered_epoch |
| def publishRegionDetections(self, scene, objects, otype, jdata): | ||
| for rname in scene.regions: | ||
| robjects = [] | ||
| for obj in objects: | ||
| if rname in obj.chain_data.regions: | ||
| robjects.append(obj) | ||
| # Region-specific detections: include sensor data | ||
| jdata['objects'] = buildDetectionsList(robjects, scene, False, include_sensors=True) | ||
| jdata['objects'] = buildDetectionsList( | ||
| robjects, scene, False, include_sensors=True, | ||
| include_region_dwell=True, current_time=get_epoch_time(jdata['timestamp'])) |
There was a problem hiding this comment.
get_epoch_time(jdata['timestamp']) is computed inside the loop over scene.regions. Since jdata['timestamp'] is the same for all regions in this publish cycle, this does repeated datetime.strptime work. Consider computing the epoch timestamp once before the loop (or passing an already-parsed epoch timestamp into this method) and reusing it for each buildDetectionsList call.
| if len(chain_data.regions): | ||
| obj_dict['regions'] = chain_data.regions | ||
| if include_region_dwell: | ||
| if current_time is None: | ||
| current_time = get_epoch_time() | ||
| obj_dict['regions'] = _build_region_output(chain_data.regions, include_region_dwell, current_time) |
There was a problem hiding this comment.
When include_region_dwell is enabled and current_time is not provided, current_time is set inside prepareObjDict. Because prepareObjDict is called once per object, different objects in the same serialized list/dict can end up with slightly different dwell values, making a single payload internally inconsistent. Consider determining current_time once in buildDetectionsList / buildDetectionsDict (or in the calling site) and passing it through for all objects.
📝 Description
Provide a clear summary of the changes and the context behind them. Describe what was changed, why it was needed, and how the changes address the issue or add value.
✨ Type of Change
Select the type of change your PR introduces:
🧪 Testing Scenarios
Describe how the changes were tested and how reviewers can test them too:
✅ Checklist
Before submitting the PR, ensure the following: