A facial recognition login service for Linux.
2

Configure Feed

Select the types of activity you want to include in your feed.

docs: Document the liveness / model-free PAD layer

Adds a "Liveness detection (model-free PAD)" section to CONTRIBUTING.md
covering: the evidence/veto composition (noisy-OR capped by veto product,
why not a weighted average, veto-only and fail-closed semantics, the
NotApplicable additivity that makes strobing structurally more secure);
the NIR reflectance gate; the removed motion-magnitude dead end and its
intended planar-homography replacement; the mandatory observe-only-until-
tuned posture and the `calibrate-liveness` workflow; and honest scope
(casual vs determined attacks, ISO/IEC 30107-3 per-attack evaluation).
Updates the "future milestone" PAD note to reflect what now exists, and
extends the defense matrix with a tuned-liveness column.

Isaac Corbrey (May 29, 2026, 7:34 AM EDT) f944223a 4c5239b5

+83 -11
+83 -11
CONTRIBUTING.md
··· 304 304 - A photograph illuminated by an NIR source matching the camera 305 305 illuminator's spectrum. 306 306 307 - Real PAD (presentation-attack detection) via a dedicated anti-spoof 308 - model is a future milestone, and would be required to defend against 309 - the high-effort attacks above. 307 + A learned, model-based PAD (presentation-attack detection) is still a 308 + future milestone and would be required to defend against the high-effort 309 + attacks above. A *model-free* PAD layer exists today as defense-in-depth 310 + — see below. 311 + 312 + ### Liveness detection (model-free PAD) 313 + 314 + Liveness — "is this a real face present now, not a photo or a screen?" — 315 + is an axis **orthogonal to identity matching**, implemented in 316 + `pareidolia_core::liveness` as a set of pluggable checks combined by a 317 + policy. It's additive hardening on top of dual-modality, *not* a 318 + replacement: on a NIR sensor, screens are largely defeated already by 319 + simply not registering a face in IR (SCRFD vetoes the IR side), and 320 + liveness raises the bar against the cases that *do* get a face. 321 + 322 + **Composition.** Each `LivenessCheck` returns a `0..1` confidence-live 323 + score and declares a `CheckRole`: 324 + 325 + - **Evidence** ("I see signs of life") combines by *noisy-OR* — more 326 + applicable evidence can only raise confidence, and a single check of 327 + score `s` contributes exactly `s` (no inflation; this rules out a 328 + weighted average, which would scale one weak signal up to full range). 329 + - **Veto** ("I see signs of a spoof") *caps* the result multiplicatively: 330 + `combined = evidence × ∏(veto)`. A confident veto drives the score to 331 + ~0 regardless of evidence. A veto-only policy reduces to the vetoes' 332 + verdict (the evidence factor is a neutral 1.0 when no evidence check 333 + applies); "nothing applicable at all" scores 0 and so fails closed at 334 + any positive threshold. 335 + 336 + A check returns `NotApplicable` (contributing nothing, capping nothing) 337 + when it can't run on the frames it got — so a strobed-only check on a 338 + constant sensor, or any check on hardware it doesn't support, is purely 339 + additive and never breaks a working config. This is also why a strobed 340 + illuminator is *structurally* more secure: it has strictly more 341 + applicable checks, so a higher achievable confidence ceiling. 342 + 343 + **Checks that exist:** 344 + 345 + - **NIR reflectance gate** (veto, single-frame): screens emit ~no 346 + near-IR (dark/flat under the sensor → vetoed); skin has a 347 + characteristic NIR reflectance and sub-surface scattering, so a real 348 + face is bright, structured, and diffusely smooth. Combines 349 + face-region brightness, structural contrast, and a Laplacian-based 350 + smoothness proxy by taking their minimum (an attack need fail only 351 + one). Measures the *detected face region*, not the whole frame. 352 + 353 + **A dead end worth recording:** raw inter-frame *motion magnitude* was 354 + tried and removed. Empirically it is anti-correlated with liveness — a 355 + hand-waved photo or a flickering screen changes *more* than a still live 356 + face — matching the PAD literature's lesson that liveness is in signal 357 + *structure*, not magnitude. The intended replacement is a 358 + planar-homography motion-residual check (rigid/planar vs 3D), not yet 359 + built. 360 + 361 + **Tuning is mandatory and data-driven.** The checks' thresholds ship as 362 + *guesses* and liveness defaults to **observe-only**: 363 + `--liveness-threshold 0.0` computes and logs the score but never rejects. 364 + You must profile real live-vs-spoof captures *on your own sensor and 365 + conditions* before raising the threshold, via `pareidolia 366 + calibrate-liveness` (capture labeled batches per attack type → report 367 + per-feature separation → suggest data-derived thresholds). A profile is 368 + only valid for similar capture conditions (notably: indoor vs daylight, 369 + since ambient near-IR shifts every brightness-based feature). 370 + 371 + **Honest scope.** Model-free liveness stops *casual* attacks (a phone, a 372 + plain printed photo) on a given sensor; it will not stop a determined 373 + attacker who tailors the artifact (NIR-reflective mask, NIR-tuned print, 374 + NIR-leaking display). Evaluate per-attack-type (ISO/IEC 30107-3 375 + APCER/BPCER), not as a single accuracy number. The framework's value is 376 + that a learned anti-spoof model drops in later as just another 377 + `LivenessCheck`. 310 378 311 379 ### Family resemblance and identity discrimination 312 380 ··· 326 394 327 395 ### Defense matrix 328 396 329 - | Attack | Single (RGB only) | Dual (RGB + IR) | 330 - | ------------------------------------- | ----------------- | --------------- | 331 - | Phone or laptop screen showing a photo | passes | rejected (SCRFD vetoes the IR side) | 332 - | Printed photo on paper | passes | likely rejected | 333 - | NIR-tuned printed photo | passes | may pass | 334 - | 3D mask with skin-like NIR | passes | may pass | 335 - | Live family member | may pass | may pass | 336 - | Same person, hardware variation | passes | passes | 397 + | Attack | Single (RGB only) | Dual (RGB + IR) | + Liveness (NIR gate, once tuned) | 398 + | ------------------------------------- | ----------------- | --------------- | --------------------------------- | 399 + | Phone or laptop screen showing a photo | passes | rejected (SCRFD vetoes the IR side) | rejected (dark/flat in IR) | 400 + | Printed photo on paper | passes | likely rejected | gate may also veto (wrong NIR reflectance) | 401 + | NIR-tuned printed photo | passes | may pass | may pass | 402 + | 3D mask with skin-like NIR | passes | may pass | may pass | 403 + | Live family member | may pass | may pass | may pass (identity confusion, not spoofing) | 404 + | Same person, hardware variation | passes | passes | passes | 405 + 406 + The liveness column assumes `--liveness-threshold` has been raised from 407 + its observe-only default of 0.0 after tuning (see *Liveness detection*); 408 + at the default it logs but never rejects. 337 409 338 410 `pareidolia test` mirrors the modes available to the daemon, so it's 339 411 useful for sanity-checking which attacks your current enrollment would