Important
This is an independent community project. It is not affiliated with, sponsored by, endorsed by, or supported by Framework Computer Inc. "Framework" and "Framework Laptop" are trademarks of Framework Computer Inc.
Framework Facecam publishes a stable 1080p virtual camera with NPU-tracked hardware face exposure and global luminance correction. Hardware-calibrated white balance, hue, and saturation remain responsible for color; face-derived chroma correction is disabled by default because skin chroma changes materially with lighting. OpenVINO prefers the NPU and falls back to CPU. OpenCV performs pixel processing on the CPU, with no spatial face filter or halo.
While streaming, the detected face drives the webcam's UVC auto-exposure region.
The module can adjust an unreported sensor-gain stage without changing
exposure_time_absolute, so face luminance may improve even when the visible
shutter control remains constant.
-
Install the system dependencies on Arch Linux:
sudo pacman -S ffmpeg v4l-utils imagemagick v4l2loopback-dkms \ openvino-intel-npu-plugin
-
Create the Python environment:
uv sync -
Calibrate while seated in the normal call position. Use an evenly lit, representative phone portrait. Any image format decodable by ImageMagick or OpenCV is accepted, including JPEG, PNG, HEIC/HEIF, WebP, and TIFF:
uv run framework-facecam calibrate reference.jpgCalibration needs neither OpenVINO model files nor an NPU. It uses OpenCV's bundled Haar cascade, sweeps conservative hue and saturation settings, and compares backlight compensation 1 and 2 while keeping hardware auto exposure enabled. It never sets manual exposure, sharpness, or UVC ROI auto exposure. Results go to
$XDG_DATA_HOME/framework-facecam/calibrations/<timestamp>and the selected controls and target go to$XDG_CONFIG_HOME/framework-facecam/config.json. To update only the target, runuv run framework-facecam calibrate-reference IMAGE.Then build a matched three-scene tone suite. Run the full hardware calibration above in
indoor-goodlighting first. For each command below, place the phone lens beside the laptop webcam and take the reference from the same seat, pose, background, and lighting. A timer with the phone's rear1xcamera is ideal. Do not use a selfie taken elsewhere; then immediately capture the matching webcam scene:uv run framework-facecam calibrate-scene indoor-good phone-good.jpg # Move to ordinary dim or uneven indoor lighting. uv run framework-facecam calibrate-scene indoor-bad phone-indoor-bad.jpg # Move outdoors, face a bright window, or create severe backlight. uv run framework-facecam calibrate-scene outdoor-bad phone-outdoor-bad.jpg uv run framework-facecam finalize-calibration
Use
--session NAMEon every command to maintain more than one suite. A scene can be rerun safely; only that scene is replaced. Suite artifacts andsession.jsonlive under$XDG_DATA_HOME/framework-facecam/calibration-suites/NAME/. Finalization writes all 21 candidate images, a labeledcontact-sheet.jpg, and a JSON score report underfinalization/. To select a reviewed candidate instead of the recommendation, use, for example:uv run framework-facecam finalize-calibration --session default --gamma 0.85Three matched references are required because a phone portrait from one lighting condition is not a universal complexion target. In particular, measured skin
b*can move substantially between scenes. Each scene keeps its own phone luminance distribution and diagnostica*/b*medians; phone chroma is never averaged into runtime configuration. White balance, hue, and saturation remain fixed global hardware controls from the indoor-good full calibration, andmax_chroma_shiftremains zero.The suite applies only the fixed global gamma candidates
1.00,0.95,0.90,0.85,0.80,0.75, and0.70to each raw webcam frame. It scores faceL*p10, median, and p90 against that scene's matching phone portrait, emphasizing the worst scene while considering mean error and mildly preferring no correction. It persists one selected value as bothmin_gammaandmax_gamma; it does not add spatial face tone or color filtering. -
Download the authoritative Open Model Zoo 2023 FP16 model and verify both files against embedded SHA256 digests:
uv run framework-facecam download-model -
Create the loopback device. The module must use
exclusive_caps=1so video clients see a capture-only camera:sudo modprobe v4l2loopback video_nr=10 exclusive_caps=1 \ card_label='Framework Facecam'
For persistent setup, install the included module files:
sudo install -m 0644 systemd/v4l2loopback.conf \ /etc/modprobe.d/framework-facecam.conf sudo install -m 0644 systemd/framework-facecam.modules.conf \ /etc/modules-load.d/framework-facecam.conf
-
Start the stream:
uv run framework-facecam run -
Select Framework Facecam in your browser or meeting application. The virtual camera publishes the source-equivalent 1920x1080 at 30 fps by default. Zen/Meet may negotiate only 720p; clients that support 1080p retain the full processed stream. To publish 720p explicitly, run:
uv run framework-facecam --output-resolution 720p run -
Optionally copy
systemd/framework-facecam.serviceto~/.config/systemd/user/, inspect its paths, then enable it yourself withsystemctl --user enable --now framework-facecam. The sample is not installed or enabled by this project.
framework-facecam capture OUTPUT writes a processed still without requiring a
loopback device. framework-facecam show-config prints defaults merged with the
persisted config. Global --input-device and --output-device options override
device paths without changing the saved config. --output-resolution selects
1080p or 720p without changing the saved output profile.
The service waits on inotify while idle and refreshes a preallocated black frame once per second so applications can discover it. The timed wait sleeps in the kernel; it is not a busy loop. Opening the virtual camera wakes it immediately, starts the physical camera, and switches to the configured live frame rate. Closing the last consumer releases the physical camera immediately.
Measured on a Framework Laptop 13 Pro with an Intel Core Ultra X7 358H,
Linux 7.2.2-cachyos, Python 3.14, OpenVINO 2026.3.1, a 1920x1080 MJPEG input,
1920x1080 I420 output, and face detection at 7.5 Hz. Each sample covered 30
seconds after warmup. CPU is expressed in logical-core units, where 1.0 means
one logical CPU fully occupied. Memory covers the entire service cgroup but
excludes the consuming OBS/browser/ffmpeg process.
| State | Delivered rate | CPU | Service memory | GPU engines | GPU GTT | NPU busy | NPU memory | NPU state |
|---|---|---|---|---|---|---|---|---|
| Idle, no consumer | 1 fps placeholder | 0.0012 cores | 170.75 MiB | 0.000000% | 3.18 MiB | 0.000000% | 66.74 MiB | 0 MHz, D3hot |
| Active, 1080p30 consumer | 28.3-28.6 fps steady | 1.0177 cores | 186.94 MiB | 0.000000% | 3.18 MiB | 3.7968% | 76.20 MiB | 950 MHz, D0 |
The service opens an Xe render descriptor through the OpenVINO/OpenCL stack, but DRM per-client counters recorded zero render, compute, blit, video, and video-enhance engine cycles during both samples. Pixel transforms run on CPU; face inference runs on NPU. Results will vary with camera format, output size, detector rate, processor, kernel, and OpenVINO version.
Reproduce a 30-second sample with:
python scripts/measure-performance.py --seconds 30Run it once with no camera consumer for idle figures, then again while OBS or a browser is actively reading Framework Facecam. The script reads service cgroup CPU/memory, Xe DRM per-client engine counters, and Intel NPU sysfs telemetry.
The input must negotiate V4L2 MJPEG at 1920x1080 and 30 fps. Face inference runs at 5-10 Hz while frames remain 30 fps. If NPU compilation is unavailable or fails, a concise log reports CPU fallback; color conversion and resizing never run on the NPU.
MIT. See LICENSE.