Tracking worker
Implementation: src/app/infrastructure/tracking/track_worker.py
Tracking a whole video takes longer than a window can stay frozen.
TrackingWorker runs the slow loop on a background thread: it reads frames in
order, asks the selected strategy to assign IDs, then repairs
the tracks with post-processing. It reports progress with
Qt signals and never touches widgets or annotation layers. Application
decides what happens to its output; see
Tracking services.
Where this sits
Solid arrows are calls; the dashed arrow is signals from the worker back to the handler. The thick box is the subject of this page.
Who creates and owns it
Solid arrows are calls; the dashed arrow is Qt signals. Only run() and the
strategy it calls execute on the worker thread.
TrackingWorkeris aQThread.start()runsrun()on the new thread.TrackingWorkerManagercreates it (withstart=True), stores it on the session, and clears Layers C and D first.- The handler then connects
progress_updated,finished_processinganderror_occurred. VideoReaderis opened insiderun()and closed in itsfinally, so decoding also stays off the GUI thread.
Construction happens on the GUI thread
The constructor deep-copies the input detections, runs nested-box
de-duplication on every frame, copies the settings, and builds the strategy
before the thread starts. On long videos with many boxes this is
GUI-thread work. A missing optional dependency (for example ultralytics
for ByteTrack) raises ModuleNotFoundError here, so it surfaces as a
start failure in the handler, not through error_occurred.
The lifecycle
Solid arrows are the normal path; dashed arrows are early exits. Every path
ends in finished_processing() because it is emitted from a finally block.
| Signal | Payload | When |
|---|---|---|
progress_updated |
(done, total) |
After each frame, and once more with the result's frame count when post-processing finishes |
error_occurred |
message | Any exception inside run() |
finished_processing |
none | Always, last |
No progress during post-processing
The frame loop reports progress, then post-processing runs silently. The handler shows an indeterminate busy bar, so the UI looks idle for a moment on long clips before the final tick.
One frame of the loop
Arrows show the order of one iteration; the dashed arrow leaves the loop.
- Reading. A
VideoReaderis created only if the session path is a real file. Otherwise tracking continues without pixels: every embedding isNoneand appearance cannot help. - Embedding. Each box is cropped and described by the appearance embedder; the vectors are passed to every strategy.
- Isolated appearance. For post-processing the worker keeps a vector only for observed boxes whose IoU with every other box in the frame is below 0.25. Overlapping subjects would otherwise contaminate each other's appearance.
Stopping and failure
stop() only sets a flag
stop() does not interrupt anything. It sets _stop_requested; the loop
checks the flag at the start of the next frame and returns. Meanwhile the
manager has already detached the worker from the session, so the stopped
worker's result is never fetched. The call sites are session close and the
export-all job; the tracking panel has no stop button. The flag is not
checked during post-processing: a stop requested then lets post-processing
run to completion.
The class docstring calls stop() "reserved for future use"; the code and
callers above show it is used.
Solid arrows are the failure paths; "no error" marks a stop that is not a failure. The handler owns the dialog.
finished_processing also fires after an error
The handler's finished_processing slot unconditionally calls
sync_tracking_cache(). After an error the worker's tracked data is still
empty, so an empty result is written into Layer C and Layer D is
refreshed from it. The user sees an error dialog and then an empty
Tracking tab. After a stop, the detached worker cannot be found, so the sync
raises TrackingWorkerNotFoundException instead.
Bad settings fail inside run()
The constructor docstring says ValueError for a bad post-processing
setting is raised at construction. In the code the
post-processor is created inside run(), after the
whole frame loop. A bad value therefore wastes the full pass and is
reported through error_occurred, not as a constructor exception.
Time flows downward. This is the unreadable-video case: a file that exists but cannot be decoded.
Nested-box de-duplication
A detector can report a torso and a full body for the same person. Two boxes
would become two tracks and two IDs. suppress_nested_same_class_boxes runs per
frame in the constructor:
- Boxes are ranked by manual origin, confidence, then area. A box is merged into a kept same-label box that is at least as large and as confident when the intersection covers at least 80 % of the smaller box (IoMin; IoU would under-rate a torso inside a body).
- Manual boxes are never suppressed.
- The suppressed box's bounds are unioned into the kept box so no pixel of the subject becomes uncovered. Inputs are cloned, not mutated.
Time flows downward. The strategy later sees one box where the detector reported two.
Numbers
A full-body box of 891 x 1690 px and a torso box of 863 x 864 px overlap on 740,448 px². IoMin is about 0.993 (above 0.80) while IoU is only about 0.49, so the torso is merged and the kept box grows 6 px to the left. The full worked arithmetic is in the function's API docstring.
Reading the result
get_tracked_data() returns a deep copy under a mutex. is_complete() is
True only after post-processing stored the result; it stays False after an
error or a stop seen in the frame loop.
Docstring correction
The class docstring says the Hungarian strategy emits "observed boxes
only". In the code it also emits coasted (predicted) boxes with
detection_backed=False; post-processing is what separates them. See
Tracking strategies.