blinklinmult.detector¶
One interface over every published model: crops in, blinks out.
The four shipped models were trained years apart, in two codebases, with different input conventions and different output shapes. Comparing them fairly means holding everything but the weights constant – the same crops, the same normalisation rule applied per model, the same event extractor, the same metrics. That is what this class is for:
from blinklinmult import BlinkDetector
v1 = BlinkDetector.from_pretrained("densenet121-union")
v2 = BlinkDetector.from_pretrained("blinkcnn")
v1.detect(crops) # [(12, 15), (48, 52)]
v2.detect(crops) # same crops, same extractor -- only the weights differ
Two methods, and the split is deliberate:
BlinkDetector.score()Per-frame closure probability. A continuous physiological signal – how shut the eye is – which carries blink shape: depth, duration, the asymmetry between a fast closing ramp and a slower reopening.
BlinkDetector.detect()Discrete
(start, end)intervals, by runningto_intervals()over that signal at the model’s own fitted operating point.
T=1 is a single frame and any larger T is video, so one object serves
both. The sequence models need real temporal context to be worth anything, but
they accept T=1 rather than failing.
Normalisation travels with the model, never with the caller. The 1.x networks
were trained on ImageNet-standardised crops and BlinkCNN on plain /255;
applying the wrong convention costs accuracy and raises nothing. Pass raw
[0, 1] crops and let MODELS decide.
Attributes¶
Module-level logger. |
Exceptions¶
Raised when a detector is built or called with something it cannot use. |
Classes¶
A published model plus the preprocessing and extraction around it. |
Module Contents¶
- blinklinmult.detector.logger¶
Module-level logger.
- exception blinklinmult.detector.DetectorError[source]¶
Bases:
ExceptionRaised when a detector is built or called with something it cannot use.
Initialize self. See help(type(self)) for accurate signature.
- class blinklinmult.detector.BlinkDetector(model: torch.nn.Module | blinklinmult.paper.session.OnnxModel, model_spec: blinklinmult.registry.ModelSpec)[source]¶
A published model plus the preprocessing and extraction around it.
Build one with
from_pretrained()rather than calling the constructor, unless wiring a model you loaded yourself.- Parameters:
- classmethod from_pretrained(model_id: str, weights: str | pathlib.Path | None = None) BlinkDetector[source]¶
Load a published model by id.
- Parameters:
model_id (str) – A key of
MODELS–densenet121-union,blinklint-union,blinklinmult-unionorblinkcnn.weights (str | Path | None) – A local checkpoint, instead of the published one.
- Returns:
Ready to score.
- Return type:
- Raises:
DetectorError – If the id is unknown or its weights cannot be loaded.
- prepare_features(features: numpy.ndarray, mean: numpy.ndarray | None = None, std: numpy.ndarray | None = None) numpy.ndarray[source]¶
Put a v2 feature vector into the layout this model expects.
Only
blinklinmult-unionneeds this, and it needs two things the v2 schema does not provide:head pose first. 1.x ordered the blocks
[pose, landmarks, iris, diameters, eyelid, ear]; v2 puts pose last.corpus-wide standardisation. 1.x z-scored each descriptor against dataset statistics. Fed raw values the model’s logits sit near -9.9 and it never fires – silently, with no error.
- Parameters:
features (np.ndarray) –
(T, 160)in the v2 layout.mean (np.ndarray | None) – Per-dimension corpus mean, in the v2 layout. Required when the model standardises.
std (np.ndarray | None) – Per-dimension corpus standard deviation.
- Returns:
(T, 160)ready to pass toscore().- Return type:
np.ndarray
- Raises:
DetectorError – If standardisation is required but no statistics were given.
- calibrate(point: blinklinmult.fit.OperatingPoint) None[source]¶
Adopt a fitted operating point in place of the registered default.
The 1.x models ship with a placeholder 0.5 threshold, never a fitted one. Once
fit_operating_point()has measured a better pair on validation data, this applies it sodetect()uses it.- Parameters:
point (OperatingPoint) – The fitted point, from
fit_operating_point().
- score(crops: numpy.ndarray | torch.Tensor, features: numpy.ndarray | torch.Tensor | None = None) numpy.ndarray[source]¶
Per-frame closure probability.
- Parameters:
crops (np.ndarray | torch.Tensor) – Crops in
[0, 1], channel-first, as(3, H, W),(T, 3, H, W)or(B, T, 3, H, W).features (np.ndarray | torch.Tensor | None) –
(T, 160)or(B, T, 160)descriptors, required only byblinklinmult-union.
- Returns:
(T,)for one window, else(B, T). Probabilitiesin
[0, 1], not logits.
- Return type:
np.ndarray
- Raises:
DetectorError – If the crop rank is unusable, or a needed feature stream is absent.
- score_long(crops: numpy.ndarray | torch.Tensor, features: numpy.ndarray | torch.Tensor | None = None, stride: int = 1) numpy.ndarray[source]¶
Score a recording longer than the model’s native window.
The 1.x sequence models were trained on 15-frame clips and lose accuracy on longer input, so a long recording is scored by sliding that window across it and averaging the overlapping predictions per frame.
Averaging, not taking the maximum. A maximum can only push a frame’s score up, so a single badly-positioned window – one where the blink sits at the very edge, an input distribution the model never trained on – raises that frame permanently and errors accumulate instead of cancelling. Measured over 200 rn30 windows carrying an annotated blink:
reduction
precision
recall
F1
stride
maximum
0.236
0.954
0.379
1
mean
0.346
0.899
0.500
1
mean
0.322
0.876
0.471
15
The maximum’s near-perfect recall against 0.24 precision is the signature of over-firing: it fires almost everywhere, so it never misses. Dense striding also beats non-overlapping windows, so
stride=1is the default despite costing more forward passes.A frame-wise model has no native window and is scored in one pass.
- Parameters:
crops (np.ndarray | torch.Tensor) –
(T, 3, H, W)in[0, 1].features (np.ndarray | torch.Tensor | None) –
(T, 160)descriptors, required only byblinklinmult-union.stride (int) – Frames between successive windows.
- Returns:
(T,)closure probability per frame.- Return type:
np.ndarray
- Raises:
DetectorError – If
strideis not positive.
- detect(crops: numpy.ndarray | torch.Tensor, features: numpy.ndarray | torch.Tensor | None = None) list[blinklinmult.train.events.Interval][source]¶
Find blink intervals in a single window.
Scores every frame, then merges runs with
to_intervals()at this model’s fitted operating point. Where a low threshold is registered the extraction is hysteretic: a run must peak above the high threshold to count, but extends while above the low one, which recovers the shallow onset and offset a single cut clips away.- Parameters:
crops (np.ndarray | torch.Tensor) – One window; see
score().features (np.ndarray | torch.Tensor | None) – Descriptors, if needed.
- Returns:
Inclusive
(start, end)frame indices.- Return type:
list[Interval]
- Raises:
DetectorError – If more than one window is passed, since intervals are indices into a single timeline.