blinklinmult.live

Rendering a live view: the frame, and a scrolling plot per eye.

What the batch pipeline draws (draw()) needs a finished Result – a whole-recording object that does not exist while frames are still arriving. So the live view draws its own, reusing blinklinmult.overlay’s colours so the two look like the same product.

Everything here is cv2 into a numpy canvas, not matplotlib. Measured, a matplotlib redraw of two small subplots costs 6.1 ms per frame against 0.13 ms for a cv2 polyline – a 47x difference, and the display loop only has about 6 ms of slack once detection and imshow are paid. A plotting library would spend a third of the remaining headroom drawing two lines.

The layout is the frame on top, and beneath it two subplots at half the frame width each: the viewer’s left eye on the left. A wink then spikes the plot on the same side as the eye that closed, which is the whole point of the view.

Attributes

DEFAULT_SECONDS

Seconds of score history each plot shows.

PLOT_HEIGHT

Height of one eye's subplot, in pixels.

PLOT_BACKGROUND

Near-white behind a plot, matching the readout panel's family.

GRID_COLOUR

Light grey for the frame around a plot.

THRESHOLD_COLOUR

Muted blue for the operating-point rule.

AXIS_COLOUR

The zero line, so the trace has something to sit on.

TRACE_COLOUR

The score trace.

TRACE_WIDTH

Polyline thickness. Two pixels reads clearly without hiding detail.

PLOT_MARGIN

Padding inside a plot, so the trace never touches the border.

LABEL_HEIGHT

Rows reserved at the top for the label and the current reading.

Classes

ScoreHistory

A bounded, per-eye history of closure scores.

Functions

draw_plot(→ numpy.ndarray)

Draw one eye's scrolling score plot.

compose(→ numpy.ndarray)

Stack the frame above the two eye plots.

draw_overlay(→ numpy.ndarray)

Annotate a live frame with the face, the eyes and a readout.

eye_crops(→ numpy.ndarray | None)

Cut both eye crops into the layout the models take.

Module Contents

blinklinmult.live.DEFAULT_SECONDS = 10.0

Seconds of score history each plot shows.

blinklinmult.live.PLOT_HEIGHT = 130

Height of one eye’s subplot, in pixels.

blinklinmult.live.PLOT_BACKGROUND = (248, 248, 248)

Near-white behind a plot, matching the readout panel’s family.

blinklinmult.live.GRID_COLOUR = (215, 215, 215)

Light grey for the frame around a plot.

blinklinmult.live.THRESHOLD_COLOUR = (120, 120, 200)

Muted blue for the operating-point rule.

Blue rather than grey: on a near-white panel a mid grey line is easy to miss, and a viewer who cannot see the threshold cannot tell whether a spike counted as a blink – which is the whole reason the rule is drawn.

blinklinmult.live.AXIS_COLOUR = (205, 205, 205)

The zero line, so the trace has something to sit on.

Without it an open eye’s near-zero score looks like an empty plot rather than a measured one.

blinklinmult.live.TRACE_COLOUR = (200, 60, 60)

The score trace.

One colour for the whole line. Colouring it by whether the score is above the threshold sounds informative and is not: a single spike at the end repaints the entire history, so the plot’s colour reports only the newest sample while appearing to describe all ten seconds. The threshold rule already shows where the operating point is, and the eye boxes on the frame carry the open/closed state.

blinklinmult.live.TRACE_WIDTH = 2

Polyline thickness. Two pixels reads clearly without hiding detail.

blinklinmult.live.PLOT_MARGIN = 6

Padding inside a plot, so the trace never touches the border.

blinklinmult.live.LABEL_HEIGHT = 16

Rows reserved at the top for the label and the current reading.

class blinklinmult.live.ScoreHistory[source]

A bounded, per-eye history of closure scores.

A deque with maxlen rather than a list: a demo left running for an hour uses exactly the same memory as one left running for a second, because the oldest sample falls off as each new one arrives.

Parameters:

capacity (int) – Samples retained – normally fps * seconds.

__post_init__() → None[source]

Allocate the bounded queue.

push(value: float) → None[source]

Add one score.

Parameters:

value (float) – Closure score in [0, 1], or nan for a frame with no usable eye. A gap, not a zero: zero means “open”, which is a claim the pipeline cannot make when it never saw the eye.

property latest: float

The most recent score.

Returns:

The last value pushed, or nan when empty.

Return type:

float

as_array() → numpy.ndarray[source]

The history, oldest first.

Returns:

(n,) float32.

Return type:

np.ndarray

blinklinmult.live.draw_plot(history: ScoreHistory, threshold: float, label: str, width: int, height: int = PLOT_HEIGHT) → numpy.ndarray[source]

Draw one eye’s scrolling score plot.

The trace fills only the fraction of the width its samples cover, so a two-second-old session does not stretch to look like a full ten seconds of data. Gaps – frames where no eye was found – break the line rather than dropping it to zero.

Parameters:
  • history (ScoreHistory) – The scores to draw.

  • threshold (float) – Operating point, drawn as a horizontal rule so a spike crossing it is visibly a blink rather than just a bump.

  • label (str) – Shown top-left, e.g. "left eye".

  • width (int) – Canvas width in pixels.

  • height (int) – Canvas height in pixels.

Returns:

(height, width, 3) uint8 RGB.

Return type:

np.ndarray

blinklinmult.live.compose(frame: numpy.ndarray, left: numpy.ndarray, right: numpy.ndarray) → numpy.ndarray[source]

Stack the frame above the two eye plots.

Parameters:
  • frame (np.ndarray) – (H, W, 3) uint8 RGB, already annotated.

  • left (np.ndarray) – The viewer’s-left eye plot.

  • right (np.ndarray) – The viewer’s-right eye plot.

Returns:

(H + plot_height, W, 3) uint8 RGB, the plots side by side beneath the frame at half its width each.

Return type:

np.ndarray

blinklinmult.live.draw_overlay(frame: numpy.ndarray, detection, scores: dict[str, float], threshold: float, lines: list[str] | None = None) → numpy.ndarray[source]

Annotate a live frame with the face, the eyes and a readout.

Parameters:
  • frame (np.ndarray) – (H, W, 3) uint8 RGB. Not modified.

  • detection – A FaceDetection from detect(), or None when no face was found.

  • scores (dict[str, float]) – Score per side; nan where unknown.

  • threshold (float) – Operating point, deciding an eye box’s colour.

  • lines (list[str] | None) – Extra readout lines, e.g. fps and head pose.

Returns:

A new annotated frame.

Return type:

np.ndarray

blinklinmult.live.eye_crops(detection, frame: numpy.ndarray, image_size: int = 64) → numpy.ndarray | None[source]

Cut both eye crops into the layout the models take.

Parameters:
  • detection – A FaceDetection from detect().

  • frame (np.ndarray) – (H, W, 3) RGB.

  • image_size (int) – Crop side the model expects.

Returns:

(2, 3, size, size) in [0, 1], or None unless both eyes were located. A one-eye batch is refused rather than scored: the caller reads the two scores positionally, so a missing side would silently shift the right eye’s score onto the left.

Return type:

np.ndarray | None