Skip to content

Session Data Store

app.infrastructure.session.session_data_store

View source code

Session-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: SessionId

parent

Optional Qt parent controlling QObject ownership.

TYPE: QObject | None DEFAULT: None

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: VideoDataLayer

frame_index

Zero-based frame index to update.

TYPE: int

box

Box to append to that frame.

TYPE: BBoxViewModel

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: VideoDataLayer

frame_index

Zero-based frame index to update.

TYPE: int

boxes

Boxes to append to that frame.

TYPE: Iterable[BBoxViewModel]

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: VideoDataLayer

target_layer_name

Editable layer receiving boxes that pass its filters.

TYPE: VideoDataLayer

session_settings

Confidence and label settings used by new_passes_filter.

TYPE: ProcessingSettings

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: bool DEFAULT: False

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: VideoDataLayer

keep_manual

Whether to retain only manual boxes in the layer.

TYPE: bool DEFAULT: False

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: list[VideoDataLayer]

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: VideoDataLayer

box_ids

Box keys (or IDs when a key is absent) to remove across frames.

TYPE: Iterable[str]

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: VideoDataLayer

box_ids

Box keys (or IDs when a key is absent) to remove.

TYPE: Iterable[str]

frame_index

Zero-based frame index to edit.

TYPE: int

RETURNS DESCRIPTION
bool

True if at least one box was removed; otherwise False.

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: VideoDataLayer

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: VideoDataLayer

frame_index

Zero-based frame index to retrieve.

TYPE: int

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: VideoDataLayer

RETURNS DESCRIPTION
bool

True if at least one frame in the layer contains a box.

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: VideoDataLayer

frame_index

Zero-based frame index to check.

TYPE: int

RETURNS DESCRIPTION
bool

True if that frame contains one or more boxes.

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: VideoDataLayer

frame_index

Zero-based frame index to check.

TYPE: int

RETURNS DESCRIPTION
bool

True if an entry exists for the frame, even when its box list is empty.

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: dict[str, dict[str, list[dict[str, object]]]]

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: VideoDataLayer

frame_index

Zero-based frame index to edit.

TYPE: int

keys

Box keys identifying which boxes to move.

TYPE: set[str]

delta_x

Horizontal translation in pixels.

TYPE: float

delta_y

Vertical translation in pixels.

TYPE: float

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: VideoDataLayer

frame_index

Zero-based frame index to edit.

TYPE: int

keys

Keys of manual boxes to move.

TYPE: set[str]

delta_x

Horizontal translation in pixels.

TYPE: float

delta_y

Vertical translation in pixels.

TYPE: float

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: VideoDataLayer

frame_index

Zero-based frame index to update.

TYPE: int

boxes

New list assigned to that frame.

TYPE: ListOfBoxes

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: VideoDataLayer

boxes_dict

Replacement box lists keyed by frame index.

TYPE: dict[int, ListOfBoxes]

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: VideoDataLayer

target_layer

Layer replaced by the copy.

TYPE: VideoDataLayer

filter_fn

Acceptance predicate; True includes a box. None accepts all source boxes. Runs under the store lock.

TYPE: Callable[[BBoxViewModel], bool] | None DEFAULT: None

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: VideoDataLayer

target_layer

Layer whose box list at index will be replaced.

TYPE: VideoDataLayer

index

Zero-based frame index.

TYPE: int

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: VideoDataLayer

min_conf

Minimum confidence for retaining non-manual boxes with a score.

TYPE: float

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: VideoDataLayer

min_conf

Minimum confidence for retaining non-manual boxes with a score.

TYPE: float

frame_index

Zero-based frame index to filter.

TYPE: int

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: VideoDataLayer

frame_index

Zero-based frame to replace.

TYPE: int

boxes

New machine-produced boxes for the frame.

TYPE: Iterable[BBoxViewModel]

filter_fn

Acceptance predicate for non-manual candidates; True includes a box and None accepts all. Runs under the store lock.

TYPE: Callable[[BBoxViewModel], bool] | None DEFAULT: None

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: VideoDataLayer

boxes_by_frame

New machine boxes keyed by zero-based frame index.

TYPE: dict[int, ListOfBoxes]

frame_count

Number of frames to refresh, covering 0..frame_count - 1.

TYPE: int

filter_fn

Acceptance predicate for non-manual candidates; True includes a box and None accepts all. Runs under the store lock.

TYPE: Callable[[BBoxViewModel], bool] | None DEFAULT: None

RAISES DESCRIPTION
ValueError

If frame_count is negative.

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.