Migrating from 1.x¶
Version 2 is a full rewrite. The 1.x public API is gone — this is a deliberate breaking change, not an oversight.
What was removed¶
# 1.x — no longer works
from blinklinmult.models import DenseNet121, BlinkLinT, BlinkLinMulT
model = DenseNet121() # weights auto-downloaded
y = model(torch.rand(32, 3, 64, 64))
blinklinmult.models is now a module rather than a package, and it loads
trained v2 checkpoints – a different thing entirely. Importing a retired name
from it raises an AttributeError naming the replacement rather than failing
opaquely.
The 1.x models are still available¶
They were not deleted; they were frozen as ONNX graphs and load by id:
from blinklinmult import BlinkDetector
model = BlinkDetector.from_pretrained("densenet121-union")
signal = model.score(crops) # (T,) eye closeness
blinks = model.detect(crops) # [(start, end), ...]
1.x class |
id today |
|---|---|
|
|
|
|
|
|
Freezing them to ONNX means they need neither torch nor linmult, so a future
dependency upgrade cannot break them – which is what happened when linmult
2.x changed its constructor and left the 1.x checkpoints unloadable.
Two differences to know. The graphs expect ImageNet-normalised crops where
blinkcnn expects plain /255, and BlinkDetector applies each model’s own
constants – so pass raw [0, 1] values. And the two sequence models were
trained on 15-frame windows; score_long slides that window and averages
the overlap.
The weight auto-download is gone¶
Constructing a model in 1.x silently fetched a checkpoint over the network from
a GitHub release; a library that reaches the internet on __init__ cannot be
used offline, cannot be pinned, and gives no way to know which weights a result
came from. Weights now download on an explicit from_pretrained call and cache
locally.
The 1.x demo scripts (webcam_densenet121.py, talkingface_inference.py) and
the 1.x preprocessing scripts are also gone; the latter are replaced by
blinklinmult.preprocess.
What replaces it¶
Models are built from a config and loaded from an explicit checkpoint:
from blinklinmult.train.module import BlinkLightningModule
module = BlinkLightningModule.load_from_checkpoint("results/joint/checkpoints/best.ckpt")
module.eval()
A checkpoint carries its own architecture config, so nothing has to be rebuilt and passed in — and the run that produced it is identified in MLflow by commit, config, and dataset hash.
Config keys that were renamed¶
1.x model configs are rejected at load with a rename hint rather than silently ignored:
1.x key |
2.x |
|---|---|
|
derived from the backbone and dataset |
|
|
|
|
|
|
|
heads are sequence-level; no time reducer |
|
|
|
|
|
derived from the dataset’s |
|
derived from the task’s targets |
|
load a checkpoint with |
The derived ones are absent by design: they are read from the data, so the model cannot disagree with what it is fed.
Behavioural changes worth knowing¶
These change results, not just APIs.
Splits are assigned by recording. 1.x had no split logic at all — it left the division to whoever ran the training script. v2 assigns splits by hashing the recording or participant, so windows from one recording cannot straddle the train/test boundary. Numbers from a 1.x run that split windows randomly are not comparable with v2’s.
Window edge cases are fixed. 1.x centred a short blink in its window, then
clamped the start to 0 and the end to the recording length — but the end clamp
set start = end - win_size without re-checking start >= 0, so a recording
shorter than one window produced a negative start and, through Python’s negative
indexing, a window stitched from the tail of the recording. Negative windows
were drawn by rejection sampling, which raised on short recordings and looped
forever on ones with no blink-free window. Both are handled explicitly and both
are covered by tests.
Eye crops are padded, not clipped. 1.x clipped a crop box to the image bounds and returned whatever was left, so an eye near the frame edge came out smaller and was then stretched by a varying amount by the subsequent resize. v2 pads instead, keeping every crop square and every eye at the same scale.
Metrics are named for what they are. 1.x reported accuracy as the headline number, which on a continuously-sampled recording is above 95% for a model that never predicts a blink. v2 reports F1, precision, recall, and average precision, and selects checkpoints on mean F1.
One model can now train both tasks. This is the point of the rewrite: with per-target validity masks, a corpus that annotates only eye state contributes to that head alone, and one run trains across all six corpora.
The evaluation set changed¶
RT-BENE / RT-GENE and ZJU were in the 1.x paper and are not in the v2 benchmark.
HUST-LEBW is included, and TalkingFace is now held out entirely as a
cross-corpus test set rather than being split. Published 1.x numbers are
therefore not directly comparable; make ablation reruns the architecture
comparison on the v2 data so the baselines and the full model are measured on
identical corpora and splits.