Исправлено (не закоммичено).
Причина: старая сетка (FaceMesh 2019, 468 точек) не закрывала веки, закрытие читалось по тёмным пикселям внутри контура глаза. На повёрнутой голове дальний глаз в тени, и закрытый глаз читался как открытый: у человека из ролика head-shake доля тёмного 48–73% на закрытых кадрах против 70–90% на открытых.
Решение: сетка заменена на модель сетки MediaPipe face landmarker (478 точек с радужками, 256×256), ONNX лежит в crates/native/models (4.9 MB, Apache-2.0), tools/convert_mediapipe.sh повторяет конвертацию из face_landmarker.task. Веки этой сетки закрываются: закрытие по зазору век (EAR) 0 на открытых кадрах и 100 на морганиях. Пиксельный метод удалён. Закрытие глаз берётся из исходной сетки, остальное — из сглаженной: сглаживание прятало моргание на повёрнутой голове.
Измерено: head-shake 2 из 2 (кадры 8–9 анфас, 47–49 при повороте 24°), blink 15 из 15, наклон 50° — 15 из 15 (было 13), остальные ролики 0–4.
Попутно под новую сетку: наклон головы (pitch) по глубине лба и подбородка — разброс по освещению 4° вместо 24°; брови и улыбка перекалиброваны, нахмуривание по внутреннему концу брови; порог присутствия лица 0.1; вероятность лица из первого скалярного выхода модели. Время: 4.19 мс на кадр (было 2.97). Блендшейпы проверены — счёт тот же на 11 из 12 роликов, но фон 0.13–0.53 на открытых глазах, поэтому выбран зазор век.
Новые фикстуры: poses/head-shake.mp4 (CC BY-SA 4.0). tools/check.sh: 15 из 15.