core: Add micro-motion liveness check (universal)
The first real liveness signal: scores involuntary frame-to-frame change
across the capture burst. A live subject always moves a little over the
~100-200ms a burst spans (head sway, micro-expression, blink); a printed
photo or a screen held up is rigid, differing only by sensor noise. So
mean per-pixel change is a model-free liveness signal — and it works on
*any* IR sensor, which is why it's the first check.
# Same-illumination comparison
The one subtlety is strobed sensors: their burst alternates lit/dark
frames, so diffing consecutive raw frames would read the strobe as huge
bogus "motion". The check therefore compares only same-illumination-phase
frames — those within `phase_band` of the lit frame's brightness. On a
constant sensor every frame qualifies (the grouping collapses to "use
all"), which is what makes the check universal. A test pins that the
strobe's dark frame is excluded.
# Applicability
Needs >= 2 same-phase frames; with fewer it returns `NotApplicable`, not
a bogus score. A minimal period-2 matching burst (one lit frame) is
therefore NotApplicable — liveness-aware burst sizing (next commit) is
what gives it enough same-phase frames. The additive model means this
just contributes nothing until then, never breaks anything.
# Scoring
Normalised mean-absolute per-pixel difference between consecutive
same-phase frames, saturating to 1.0 at `MicroMotionConfig::saturation`.
The constants are first-cut estimates to be tuned from real
live-vs-spoof captures (guided calibration), which is why liveness stays
observe-only (threshold 0.0) until then.
Proptested: total + bounded over arbitrary frames, and monotonic in
motion (more per-pixel change never lowers the score). Also records the
planned complementary checks (embedding-agreement, texture, differencing)
in the module docs.