Session Data Store
app.infrastructure.session.session_data_storeSession-owned annotation storage indexed by layer and frame.
Implements the
SessionDataStoreInterface
contract for the four
VideoDataLayer
values. Store operations use a reentrant lock, but read methods can expose live
mutable objects; callers must respect the ownership and locking rules below.
Layer: Infrastructure. Context: Implements concrete processing and I/O behind application-facing contracts.
SessionDataStore
Bases: QObject
Store a session's detection and tracking boxes by layer and frame index.
Architecture
- Structure:
dict[VideoDataLayer, dict[int, ListOfBoxes]]. - Layers: A (raw detections), B (user-edited detections), C (raw tracks), and D (user-edited tracks).
- Missing frames and initialized frames with no boxes are distinct states.
- Layer roles are workflow conventions, not access restrictions enforced by this store; mutation methods accept any layer.
Live data is not a thread-safe snapshot
Getters release the lock before returning lists or mappings owned by the
store. Coordinate subsequent access with data_lock or use store methods
to mutate annotations. Replacement operations can detach previously
returned lists. Box objects may also be shared with callers, depending on
the write method. A lock does not provide rollback on failure.
Frame indices are conventionally zero-based; this store does not generally validate bounds against a video. Manual-box preservation is method-specific, not a guarantee of every clear, overwrite, or delete operation.
Examples:
Given an initialized store and an iterable of detections:
from app.domain.video.layer import VideoDataLayer
store.add_boxes_to_layer_at_frame_index(VideoDataLayer.A, 0, detections)
store.overwrite_source_layer_to_target_layer(VideoDataLayer.A, VideoDataLayer.B)
with store.data_lock:
count = len(store.get_boxes_for_layer_at_frame_index_as_list(VideoDataLayer.B, 0))
data_lock: threading.RLock
property
Return the lock used to coordinate access to stored annotation data.
| RETURNS | DESCRIPTION |
|---|---|
RLock
|
The existing reentrant lock, not an acquired lock or a new instance. |
Use it as a context manager when accessing live data across multiple calls. Acquiring it does not protect against callers that bypass the locking contract.
__init__(s_id: SessionId, parent: QObject | None = None) -> None
| PARAMETER | DESCRIPTION |
|---|---|
s_id
|
Identifier of the session owning this store.
TYPE:
|
parent
|
Optional Qt parent controlling QObject ownership.
TYPE:
|
add_box_to_layer_at_frame_index(layer: VideoDataLayer, frame_index: int, box: BBoxViewModel) -> None
Append one box to a frame, initializing its entry when absent.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer receiving the box.
TYPE:
|
frame_index
|
Zero-based frame index to update.
TYPE:
|
box
|
Box to append to that frame.
TYPE:
|
The box is stored by reference; later in-place changes affect callers sharing that object.
add_boxes_to_layer_at_frame_index(layer: VideoDataLayer, frame_index: int, boxes: Iterable[BBoxViewModel]) -> None
Append boxes to a frame without cloning their objects.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer receiving the boxes.
TYPE:
|
frame_index
|
Zero-based frame index to update.
TYPE:
|
boxes
|
Boxes to append to that frame.
TYPE:
|
Falsy containers are ignored without initializing a frame. Other iterables are consumed under the lock; an empty iterator can initialize an empty entry, and an iterator error can leave a partially extended list.
apply_filters_to_layers(source_layer_name: VideoDataLayer, target_layer_name: VideoDataLayer, session_settings: ProcessingSettings) -> int
Re-filter source boxes into the target layer using the current session settings.
| PARAMETER | DESCRIPTION |
|---|---|
source_layer_name
|
Layer containing the unfiltered boxes to evaluate.
TYPE:
|
target_layer_name
|
Editable layer receiving boxes that pass its filters.
TYPE:
|
session_settings
|
Confidence and label settings used by
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
int
|
Number of frames whose target-layer box count changed and was replaced. |
Note
Existing manual boxes in each target frame are cloned and retained.
Source boxes that pass new_passes_filter are cloned, then combined
with the retained manual clones. Only source frame indices are
visited; missing target entries are initialized as empty.
Count-based replacement
A target list is replaced only when its resulting box count differs. Equal-count changes to identity, coordinates, or content are not applied. Source and target should be distinct layers. Filter or clone failures propagate and may leave earlier frames updated.
clear_all_layers(keep_manual: bool = False) -> None
Clear all layers while holding the lock across the operation.
| PARAMETER | DESCRIPTION |
|---|---|
keep_manual
|
Whether to retain manual boxes in editable layers B and D.
TYPE:
|
Layers A and C are always fully cleared. When preserving manual boxes, B/D frame entries remain present, even if their lists become empty.
clear_layer_by_name(layer: VideoDataLayer, keep_manual: bool = False) -> None
Clear a layer, optionally retaining its manual boxes.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer to clear.
TYPE:
|
keep_manual
|
Whether to retain only manual boxes in the layer.
TYPE:
|
With keep_manual=False, frame entries are removed. Otherwise existing
entries remain, their lists are replaced, and retained boxes are not
cloned. This method does not restrict manual preservation to B/D.
clear_layers_by_name(layers: list[VideoDataLayer]) -> None
Clear each requested layer using separate lock acquisitions.
| PARAMETER | DESCRIPTION |
|---|---|
layers
|
Layers whose contents should be cleared.
TYPE:
|
The sequence is not atomic across layers. Callers needing a single
critical section can hold data_lock around the call.
delete_boxes_from_layer_by_id(layer: VideoDataLayer, box_ids: Iterable[str]) -> None
Delete boxes whose key matches a requested identifier across frames.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer from which matching boxes are removed.
TYPE:
|
box_ids
|
Box keys (or IDs when a key is absent) to remove across frames.
TYPE:
|
The iterable is consumed once into a set. An empty set does nothing.
An existing empty key does not fall back to id. Frame entries remain
initialized; retained box objects are not cloned.
delete_boxes_from_layer_by_id_at_frame_index(layer: VideoDataLayer, box_ids: Iterable[str], frame_index: int) -> bool
Delete boxes with matching keys from one existing frame.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer from which boxes are removed.
TYPE:
|
box_ids
|
Box keys (or IDs when a key is absent) to remove.
TYPE:
|
frame_index
|
Zero-based frame index to edit.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
bool
|
|
An absent frame is not initialized. An existing empty key does not fall
back to id. When boxes are removed, the frame list is replaced without
cloning retained objects.
get_all_boxes_for_layer_as_dict_of_lists(layer: VideoDataLayer) -> dict[int, ListOfBoxes]
Return the store's frame-to-box mapping for a layer.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer whose frame mapping is returned.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
dict[int, ListOfBoxes]
|
The live mapping from frame indices to box lists; callers should use |
dict[int, ListOfBoxes]
|
store methods rather than mutating this mapping directly. |
Lock lifetime
The method releases its lock before returning. Hold data_lock
around subsequent iteration when concurrent store writes are possible.
Indexing a missing key in this defaultdict creates an empty entry.
get_boxes_for_layer_at_frame_index_as_list(layer: VideoDataLayer, frame_index: int) -> ListOfBoxes
Retrieve the stored box list for a frame without cloning it.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer whose frame data is requested.
TYPE:
|
frame_index
|
Zero-based frame index to retrieve.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
ListOfBoxes
|
The live stored list, or a new empty list if the frame has no entry. |
ListOfBoxes
|
Reading an absent frame does not initialize its entry. |
Note
An existing list is returned directly, not cloned. Use store mutation methods when changing annotations so callers follow the locking contract.
has_boxes_for_layer(layer: VideoDataLayer) -> bool
Check whether any frame in a layer contains boxes.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer to check.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
bool
|
|
has_boxes_for_layer_at_frame_index(layer: VideoDataLayer, frame_index: int) -> bool
Check whether a frame contains at least one box.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer to check.
TYPE:
|
frame_index
|
Zero-based frame index to check.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
bool
|
|
has_frame_for_layer_at_frame_index(layer: VideoDataLayer, frame_index: int) -> bool
Return whether a layer frame was initialized, even when it is intentionally empty.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer to check.
TYPE:
|
frame_index
|
Zero-based frame index to check.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
bool
|
|
Unlike the box-presence checks, this distinguishes an initialized empty frame from one that has never been stored.
load_project_payload(payload: dict[str, dict[str, list[dict[str, object]]]]) -> None
Replace stored layers with box data from a project payload.
| PARAMETER | DESCRIPTION |
|---|---|
payload
|
Mapping from layer names to frame-index strings and serialized box mappings.
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If a layer name, frame-index string, or serialized box source cannot be parsed. |
KeyError
|
If a serialized box is missing a required field. |
TypeError
|
If a frame key cannot be converted to an integer or box coordinates cannot be iterated. |
Loading is not transactional
All four layers are cleared before parsing begins. Conversion errors propagate and may leave partially loaded data; the old contents are not restored. Validate external project data before calling.
Frame keys are passed to int() without checking video bounds. Empty
lists create initialized entries, omitted layers remain empty, and box
objects are reconstructed rather than shared with the payload.
move_boxes_in_layer_at_frame_index_by_delta(layer: VideoDataLayer, frame_index: int, keys: set[str], delta_x: float, delta_y: float) -> int
Move boxes by (delta_x, delta_y) pixels, return count moved.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer containing the boxes.
TYPE:
|
frame_index
|
Zero-based frame index to edit.
TYPE:
|
keys
|
Box keys identifying which boxes to move.
TYPE:
|
delta_x
|
Horizontal translation in pixels.
TYPE:
|
delta_y
|
Vertical translation in pixels.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
int
|
Number of matching boxes whose coordinates were updated. |
Updates shared box objects in place, converting each translated
coordinate with int() and without clipping to video bounds. Looking up
an absent frame initializes an empty entry.
move_manual_boxes_in_layer_at_frame_index_by_delta(layer: VideoDataLayer, frame_index: int, keys: set[str], delta_x: float, delta_y: float) -> int
Move only matching boxes whose is_manual property is true.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer containing the boxes.
TYPE:
|
frame_index
|
Zero-based frame index to edit.
TYPE:
|
keys
|
Keys of manual boxes to move.
TYPE:
|
delta_x
|
Horizontal translation in pixels.
TYPE:
|
delta_y
|
Vertical translation in pixels.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
int
|
Number of matching manual boxes whose coordinates were updated. |
Coordinates are updated in place using int() after translation, without
clipping to video bounds. An absent frame is initialized as empty.
overwrite_layer_at_index_with_boxes(layer: VideoDataLayer, frame_index: int, boxes: ListOfBoxes) -> None
Replace the stored box list for one layer and frame.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer whose frame entry is replaced.
TYPE:
|
frame_index
|
Zero-based frame index to update.
TYPE:
|
boxes
|
New list assigned to that frame.
TYPE:
|
Note
The supplied list and box objects are stored directly, not cloned.
overwrite_layer_with_dict_of_boxes(layer: VideoDataLayer, boxes_dict: dict[int, ListOfBoxes]) -> None
Replace a layer with lists keyed by zero-based frame index.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer whose stored frame data is cleared and replaced.
TYPE:
|
boxes_dict
|
Replacement box lists keyed by frame index.
TYPE:
|
Note
Empty lists are omitted from storage. Each non-empty list is shallow copied; its box objects are not cloned.
Supply a mapping independent of this layer's live mapping: the layer is cleared before input iteration. Failures during iteration/copying propagate without restoring the previous contents.
overwrite_source_layer_to_target_layer(source_layer: VideoDataLayer, target_layer: VideoDataLayer, filter_fn: Callable[[BBoxViewModel], bool] | None = None) -> None
Copy all accepted boxes from a source layer to a target layer.
| PARAMETER | DESCRIPTION |
|---|---|
source_layer
|
Layer providing box data.
TYPE:
|
target_layer
|
Layer replaced by the copy.
TYPE:
|
filter_fn
|
Acceptance predicate;
TYPE:
|
Destructive replacement
Use distinct source and target layers: the target is cleared first, so copying a layer onto itself clears its source too. Predicate or cloning errors propagate and may leave the target partially written.
Only non-empty source frames are visited. Accepted boxes are cloned; a predicate rejecting every box still produces an empty target entry for that non-empty source frame.
Examples:
Given an initialized store:
store.overwrite_source_layer_to_target_layer(VideoDataLayer.A, VideoDataLayer.B)
Import VideoDataLayer from app.domain.video.layer.
overwrite_source_to_target_layer_at_index(source_layer: VideoDataLayer, target_layer: VideoDataLayer, index: int) -> None
Copy-and-overwrite a single frame's boxes from one layer to another.
| PARAMETER | DESCRIPTION |
|---|---|
source_layer
|
Layer providing source boxes.
TYPE:
|
target_layer
|
Layer whose box list at
TYPE:
|
index
|
Zero-based frame index.
TYPE:
|
Boxes are cloned before replacing the target list. An absent or empty source produces an initialized empty target entry. Passing the same layer clones that frame in place; cloning errors leave its old list intact.
purge_boxes_in_layer_by_minimum_confidence(layer: VideoDataLayer, min_conf: float) -> None
Remove scored, non-manual boxes below min_conf across stored frames.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer from which low-confidence machine boxes are removed.
TYPE:
|
min_conf
|
Minimum confidence for retaining non-manual boxes with a score.
TYPE:
|
Note
Manual boxes and boxes without a confidence score are retained. Each frame list is replaced while retained box objects keep their identity. No new frame indices are introduced.
purge_boxes_in_layer_by_minimum_confidence_at_frame_index(layer: VideoDataLayer, min_conf: float, frame_index: int) -> None
Remove scored, non-manual boxes below a frame's confidence threshold.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Layer from which low-confidence machine boxes are removed.
TYPE:
|
min_conf
|
Minimum confidence for retaining non-manual boxes with a score.
TYPE:
|
frame_index
|
Zero-based frame index to filter.
TYPE:
|
Note
Manual boxes and boxes without a confidence score are retained. The stored list is replaced, but retained box objects are not cloned. An absent frame is initialized with an empty list.
replace_machine_boxes_at_frame_index(layer: VideoDataLayer, frame_index: int, boxes: Iterable[BBoxViewModel], filter_fn: Callable[[BBoxViewModel], bool] | None = None) -> None
Replace machine boxes in one frame while retaining manual annotations.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Target layer whose machine boxes are refreshed.
TYPE:
|
frame_index
|
Zero-based frame to replace.
TYPE:
|
boxes
|
New machine-produced boxes for the frame.
TYPE:
|
filter_fn
|
Acceptance predicate for non-manual candidates;
TYPE:
|
Note
Manual boxes remain in their current order and retain their identity;
each accepted machine box is cloned to keep layers independently editable.
Manual candidates in boxes are ignored. An empty result still
initializes the frame. Iteration, predicate, and cloning errors
propagate before the replacement list is assigned.
Examples:
Given store, candidate detections, and VideoDataLayer imported
from app.domain.video.layer:
store.replace_machine_boxes_at_frame_index(VideoDataLayer.B, 0, detections)
replace_machine_boxes_for_frames(layer: VideoDataLayer, boxes_by_frame: dict[int, ListOfBoxes], frame_count: int, filter_fn: Callable[[BBoxViewModel], bool] | None = None) -> None
Refresh all machine boxes in a frame range under one lock acquisition.
| PARAMETER | DESCRIPTION |
|---|---|
layer
|
Target layer whose machine boxes are refreshed.
TYPE:
|
boxes_by_frame
|
New machine boxes keyed by zero-based frame index.
TYPE:
|
frame_count
|
Number of frames to refresh, covering
TYPE:
|
filter_fn
|
Acceptance predicate for non-manual candidates;
TYPE:
|
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If |
Note
Existing manual boxes are retained on every frame, including frames with no machine detections. Frames outside the requested range are unchanged. Every frame in the range is initialized, even when empty. A zero count performs no updates; input entries outside the range are ignored.
No rollback
Iteration, predicate, or cloning errors propagate. Earlier frames remain updated even though the failing frame is not replaced.
Examples:
Given store, a tracked frame mapping, the video frame_count,
and VideoDataLayer imported from app.domain.video.layer:
store.replace_machine_boxes_for_frames(VideoDataLayer.D, tracked, frame_count)
to_project_payload() -> dict[str, dict[str, list[dict[str, object]]]]
Serialize session layer data into the project payload structure.
| RETURNS | DESCRIPTION |
|---|---|
dict[str, dict[str, list[dict[str, object]]]]
|
Nested mapping of layer name to frame-index strings to serialized box dictionaries. |
All four layer keys are included; empty frames are omitted. Serialization creates new dictionaries and coordinate lists under one lock acquisition. It does not write a file or preserve initialized-empty frame entries.