Skip to content

About

Howdy face authentication in complete darkness on Windows Hello IR hardware, with model auditing tools (Ubuntu 26.04)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

howdy-ir-setup

Getting Howdy face authentication working reliably in complete darkness on a Dell laptop with Windows Hello infrared hardware, on Ubuntu 26.04.

This is a record of an actual debugging session, including the dead ends. The scripts in scripts/ are reusable on any machine with an IR camera.


The problem, and what it actually was

Two symptoms, which looked unrelated:

  1. Face recognition failed in low light.
  2. The small red light next to the camera — which blinked under Windows Hello — never came on.

Both had the same cause, and the second one explains the first.

A laptop with Windows Hello has two cameras behind one bezel:

Device Sensor Formats
/dev/video0 RGB webcam MJPG, YUYV
/dev/video2 Infrared GREY

Howdy's default config pointed at /dev/video0, the ordinary colour webcam. That sensor needs ambient visible light, so a dark room meant no recognition.

The "red indicator light" is not a status LED. It is the IR illuminator — an array of infrared LEDs that floods your face so the IR sensor can see. It sits just at the edge of human visible range, which is why it reads as a dim red flicker. It only energises when something opens the IR camera stream. Windows Hello used the IR camera, so it blinked. Howdy on /dev/video0 never touched it.

So there was nothing to implement for the blinking light. Pointing Howdy at /dev/video2 fixed the darkness problem, and the blink returned as a side effect of the same change.

Things that sound reasonable but do not work

Brightening the screen to light your face. The IR sensor sits behind an IR-pass filter and images ~850nm light from the illuminator. Visible light from a display is filtered out before it reaches the sensor. Once you are on the IR camera, ambient visible light stops being a variable entirely — which is the whole point.

Raising certainty when recognition is unreliable. certainty is the match threshold. Raising it does not improve recognition, it widens what counts as your face. Every problem in this session was fixed at the capture layer instead, and the threshold never moved off its default of 3.5.


Verified environment

Everything below was confirmed on the machine this was built for:

  • Ubuntu 26.04.1 LTS, kernel 7.0.0, Python 3.14
  • Dell Integrated_Webcam_HD, IR sensor at /dev/video2, GREY 340x340
  • Howdy built from source, boltgolt/howdy @ d3ab993 (v2.6.1-273, master)
  • dlib 20.0.1 via pip, python3-opencv 4.10.0 and python3-numpy 2.3.5 via apt

Howdy is not in the Ubuntu archives, so it is built from source with meson.


Installation

1. Build dependencies

sudo apt update
sudo apt install -y \
    build-essential cmake meson ninja-build pkg-config \
    python3-dev python3-opencv python3-numpy \
    libinih-dev libevdev-dev libpam0g-dev \
    v4l-utils

v4l-utils is not a Howdy dependency, but v4l2-ctl is needed to find the IR camera.

2. dlib

Ubuntu 26.04 enforces PEP 668, so installing into the system interpreter needs an explicit override:

sudo pip3 install dlib --break-system-packages

This compiles from source and takes several minutes. Confirm it worked:

python3 -c "import dlib; print(dlib.__version__)"

3. Build and install Howdy

git clone https://github.com/boltgolt/howdy.git
cd howdy
meson setup build
ninja -C build
sudo ninja -C build install

This installs to the /usr/local prefix:

Path Contents
/usr/local/bin/howdy CLI entry point
/usr/local/lib/x86_64-linux-gnu/howdy/ Python implementation
/usr/local/lib/x86_64-linux-gnu/security/pam_howdy.so PAM module
/usr/local/etc/howdy/config.ini Configuration
/usr/local/etc/howdy/models/ Enrolled face models
/usr/local/share/dlib-data/ dlib recognition models

4. Wire up PAM

The build defaults to install_pam_config=false, so the PAM line is added by hand. Edit /etc/pam.d/common-auth and add this above the pam_unix.so line:

auth	sufficient	/usr/local/lib/x86_64-linux-gnu/security/pam_howdy.so

Order matters. sufficient means a successful face match ends authentication immediately, and a failure falls through to the next module. Placing it above pam_unix.so makes face the first attempt with password as fallback.

Resulting stack:

auth	sufficient	/usr/local/lib/x86_64-linux-gnu/security/pam_howdy.so
auth	[success=2 default=ignore]	pam_unix.so nullok try_first_pass
auth	[success=1 default=ignore]	pam_sss.so use_first_pass
auth	requisite			pam_deny.so

Keep a root shell open while editing PAM. A malformed common-auth can lock you out of authentication entirely. Because Howdy is sufficient rather than required, a Howdy failure falls through to your password — but a syntax error in the file is a different matter.


Configuration

Find the IR camera:

./scripts/detect-ir-camera.sh

It reports every capture node with its formats and flags the GREY-only one. Then apply the settings that matter, in /usr/local/etc/howdy/config.ini:

[video]
device_path = /dev/video2   # the IR camera, NOT the RGB one
device_fps = 30             # sync capture to the IR emitter pulse
certainty = 3.5             # default; leave it alone
timeout = 6                 # IR emitter needs a moment to energise
dark_threshold = 60         # default; see troubleshooting

Why device_fps matters

This is the setting that caused the most trouble, and it is easy to miss.

The IR illuminator pulses. At the default -1, OpenCV picks a capture rate that is not synchronised to it, so some frames are caught between pulses and come out dark or half-lit. Encode one of those and you get a model that does not resemble your face.

That is exactly what happened here. The very first enrolled model was captured before the emitter stabilised. Its nearest neighbour among 13 later models was 5.42 — far outside the 3.5 threshold — so it never matched anything. Not realising that, the fix attempted at the time was enrolling more and more models until enough good ones accumulated.

Pinning device_fps = 30 made captures consistent. The improvement in the glasses models was immediate:

before after
spread between glasses models 4.15 – 4.81 2.66

Set this before enrolling, not after.


Enrolling faces

sudo howdy add

Roughly 5–6 models per appearance is enough. Vary your head angle slightly between scans — near-identical captures cost matching time without adding coverage.

Glasses need their own models

Glasses are not a minor variation under IR. The illuminator reflects off the lenses, often showing them as bright opaque patches, so the encodings land in a genuinely separate cluster several units away from bare-face ones.

Label them distinctly (...Glasses1, ...Glasses2, ...). analyze-models.py uses that substring to group models, and grouping is not cosmetic: scoring every model against every other would flag the entire glasses cluster as outliers and recommend deleting exactly the models you need.

Tip that reduced glare noticeably: angle your chin down slightly, so the emitter reflects off the lens away from the sensor rather than straight back.


Auditing your models

sudo ./scripts/analyze-models.py

It groups models by label, then reports, within each group, how far each model sits from its neighbours on Howdy's scale (dlib distance x 10, lower is closer).

=== WITH 'Glasses' (5 models) ===
id    label                    avg       nearest   to
--------------------------------------------------------------
15    Prince FaceGlasses6      3.01      2.06      16
16    Prince FaceGlasses7      3.11      2.06      15
14    Prince FaceGlasses5      3.33      2.66      15
17    Prince FaceGlasses8      3.71      3.33      16
18    Prince FaceGlasses9      3.88      3.59      14    ISOLATED

Two things to read from this:

ISOLATED — nearest neighbour is beyond the threshold. That model only covers the exact pose it was captured at, so tilting your head slightly drops you into a gap. Usually a dark or glare-affected frame. Re-capture it.

redundant — nearest neighbour is under 1.7, a near-duplicate. Harmless, but removable if you want fewer encodings to compare against.

The report also checks whether the two groups overlap. If the closest bare-to-glasses pair is under the threshold, coverage is continuous between them rather than two disconnected islands.

Remove a model with sudo howdy remove <id>.


Tidying labels

howdy remove never reuses ids, so pruning leaves gaps (1, 2, 4, 5, 7...) and any numbering in your labels drifts out of sync.

sudo ./scripts/relabel-models.py --prefix "Face" --dry-run   # preview
sudo ./scripts/relabel-models.py --prefix "Face"             # apply

It renumbers ids to a gapless 0..N, rebuilds labels to match, and keeps the grouped models last. Only id and label change — encodings and timestamps are copied through, verified unchanged before writing, and a timestamped backup is made first. Nothing outside the model file references these ids.


Testing

There are two distinct things to test, and passing one says nothing about the other.

Camera and recognition:

sudo howdy test

Opens a live preview with a running certainty score. Below 3.5 is a match. This exercises the camera and the models, but does not touch PAM. Run it once in normal light and again in a dark room — under IR the two should look nearly identical.

Actual authentication:

sudo -k; sudo echo "face auth worked"

sudo -k clears the cached credential first. Without it you are waved through from your last password entry and learn nothing.

Use ; rather than && — sudo -k can return non-zero even on success.

Also lock the session (Super+L) and confirm unlock by face, since a screen locker can handle PAM differently than sudo does.


Troubleshooting

All frames were too dark, please check dark_threshold in config

Average darkness: [62.73], Threshold: 60.0

dark_threshold rejects frames caught between IR emitter pulses. A marginal reading like the above usually succeeds on a retry. If it recurs, loosen it slightly:

dark_threshold = 68

Raise it cautiously. A higher value admits genuinely underlit frames, and an underlit frame is precisely how a useless model gets enrolled.

Authentication breaks after a reboot

/dev/videoN numbering can shuffle on USB-attached camera modules. Re-run ./scripts/detect-ir-camera.sh and check the GREY device is still where device_path points.

Recognition works, but inconsistently

Run analyze-models.py. Isolated models mean patchy coverage — re-capture them. Do not raise certainty to compensate.

PAM timeout, falling back to password

[sudo error] Failure, timeout reached

Confirm device_path is the IR device, device_fps is pinned, and consider raising timeout to 6. The emitter needs a moment to energise after the stream opens.


Security notes

Model files are biometric data. /usr/local/etc/howdy/models/*.dat contains 128-dimension encodings of your face. The .gitignore here excludes *.dat — do not commit them.

Howdy is not equivalent to a password. It authenticates against a 2D IR image with no depth sensing or liveness detection, so it is weaker than Windows Hello, which uses structured-light depth. Reasonable for sudo and unlocking a screen; think carefully before more than that.

Junk models are a real risk. A model built from a dark or garbled frame is not inert — it is a live match target corresponding to noise rather than your face. Anything landing near it authenticates. This is the main reason to run analyze-models.py after enrolling.

Never raise certainty to fix recognition problems. It trades security for convenience, and in every case here the real fault was capture quality.


Repository layout

howdy-ir-setup/
├── README.md
├── .gitignore                    # excludes *.dat biometric models
└── scripts/
    ├── detect-ir-camera.sh       # find the IR capture node
    ├── analyze-models.py         # cluster-aware model audit
    └── relabel-models.py         # renumber ids and sync labels

Licence

MIT. Howdy itself is MIT licensed and maintained at boltgolt/howdy.

About

Howdy face authentication in complete darkness on Windows Hello IR hardware, with model auditing tools (Ubuntu 26.04)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages