blinklinmult.stream¶
Running a sequence model on a live stream without stalling it.
The 1.x sequence models predict frame-wise but consume a 15-frame window, and one call over both eyes costs 77.1 ms measured on CPU – against a 33.3 ms budget at 30 fps. Scoring every frame is not expensive, it is arithmetically impossible: the call alone is 2.3x the whole budget before a face has been detected. Thread tuning does not rescue it either (74.4 ms at four intra-op threads against 76.9 on auto), because the model is compute-bound.
So the two loops are decoupled rather than budgeted. Capture, detect and
display run at full speed on the calling thread; a single background worker
scores the most recent window whenever it is free. That works because
onnxruntime releases the GIL during run() – measured, a busy main loop kept
102% of its idle iteration count with a worker running flat out, and face
detection went from 10.3 to 10.6 ms/frame. The worker is genuinely concurrent,
not time-slicing.
The stride is adaptive, not configured. The worker finishes a window, takes the newest frames, and starts again. A fast machine gets denser coverage and a slow one degrades gracefully, where a fixed stride has to be re-tuned per machine and is wrong in both directions everywhere else. Measured against scoring every frame on a real recording, correlation is 0.999 at stride 2 and 0.996 at stride 3, and the peak score on annotated closed frames never drops at any stride – blinks are still caught, it is the shape of the signal between them that thins out.
The readout therefore trails the video, by roughly the window fill plus one
inference – about 250 ms in practice. StreamScore carries its own age
so a caller can display that honestly rather than pretend the number is current.
Frames newer than the last completed window keep the previous value; they are
never interpolated, because that would invent scores the model did not produce.
Attributes¶
Module-level logger. |
|
How long |
|
Pause when the buffer is not yet full. |
|
What |
Classes¶
One completed window's scores, and how stale they are. |
|
Scores a live stream with a model that needs a window of frames. |
Module Contents¶
- blinklinmult.stream.logger¶
Module-level logger.
- blinklinmult.stream.JOIN_TIMEOUT_SECONDS = 5.0¶
How long
StreamingScorer.stop()waits for the worker to finish.Generous next to one inference (~77 ms), so a clean shutdown is the normal case and the timeout only fires if the scoring function itself has hung.
- blinklinmult.stream.IDLE_SLEEP_SECONDS = 0.002¶
Pause when the buffer is not yet full.
Short enough to be invisible against a 33 ms frame interval, long enough that a warming-up scorer does not spin a core.
- class blinklinmult.stream.StreamScore[source]¶
One completed window’s scores, and how stale they are.
- Parameters:
scores (np.ndarray) –
(n_eyes,)per-eye score for the window’s last frame – what a live readout displays.frame_index (int) – Index of the newest frame the window covered.
age_frames (int) – Frames pushed since that window closed. The lag, reported rather than hidden: a sequence model that keeps up visually while showing half-second-old state is a different product from one that does not.
windows_completed (int) – Windows scored since the scorer started.
failures (int) – Windows the scoring function raised on.
- property warming_up: bool¶
Whether no window has completed yet.
- Returns:
Truebefore the first score exists, so a caller shows “waiting” rather than a fabricated zero.- Return type:
bool
- blinklinmult.stream.EMPTY_SCORE¶
What
StreamingScorer.latest()returns before the first window closes.
- class blinklinmult.stream.StreamingScorer(score: collections.abc.Callable[[numpy.ndarray], numpy.ndarray], window: int | None)[source]¶
Scores a live stream with a model that needs a window of frames.
Push one frame at a time from the capture loop; read the most recent completed score whenever the display needs it. Neither call waits for inference.
For a frame-wise model there is nothing to decouple – pass
window=Noneand every push is scored inline, which is both simpler and lower latency than handing a 15 ms call to a thread.- Parameters:
score (Callable[[np.ndarray], np.ndarray]) – Takes
(n_eyes, window, 3, H, W)(or(n_eyes, 3, H, W)whenwindowisNone) and returns per-frame scores. Normallyscore().window (int | None) – Frames the model consumes, from
window.Nonescores inline.
Example
>>> scorer = StreamingScorer(detector.score, window=15) >>> scorer.start() >>> scorer.push(crops) >>> scorer.latest().warming_up True
Set up the buffer and the worker’s state.
- property threaded: bool¶
Whether scoring happens on a background worker.
- Returns:
Falsefor frame-wise models, which score inline.- Return type:
bool
- start() None[source]¶
Start the worker, if this model needs one.
Idempotent: starting an already-running scorer does nothing.
- stop() None[source]¶
Ask the worker to finish and wait for it.
Safe to call twice, and safe to call on a scorer that never started.
- push(crops: numpy.ndarray) None[source]¶
Hand one frame’s eye crops to the scorer.
Returns immediately in the threaded case – the frame is buffered and the worker picks it up when free. This is the property the whole design rests on: the capture loop must never wait for a 77 ms inference.
- Parameters:
crops (np.ndarray) –
(n_eyes, 3, H, W)for one frame.
- latest() StreamScore[source]¶
The most recent completed score.
- Parameters:
None
- Returns:
The last window’s result with its current age, or
EMPTY_SCOREwhile warming up. Frames newer than that window carry this same value rather than an interpolated one.- Return type:
- __enter__() StreamingScorer[source]¶
Start the worker on entry.
- Returns:
This scorer.
- Return type: