Fix Gaze Face Recognition After Reboot on the MECHREVO Wujie 15X Pro

Notes

This post was written with help from AI based on my actual troubleshooting, system logs, and tests on the laptop.

I recently set up Linux face recognition on a MECHREVO Wujie 15X Pro and ran into a strange problem. Gaze, PAM, the RGB and IR cameras, and my face template all looked fine. Face recognition worked right after setup, but after a full reboot Gaze GUI, sudo, the KDE lock screen, and the login screen all failed to recognize me.

The cameras themselves did not report an obvious error. RGB opened, IR opened, and the logs showed an infrared image, but authentication always ended with matched=false.

After ruling out the Gaze model, PAM, PipeWire, the face template, and USB autosuspend, I found what appears to be a cold-start initialization issue with the SunplusIT RGB/IR camera module in this laptop: after a cold boot, the RGB camera needs to stream for a few seconds before the IR camera works normally.

I do not know where the manufacturer sourced this camera module, but on my laptop it needs this extra step under Linux.

The issue reproduces consistently on my Wujie 15X Pro. I have not tested enough units to know whether every machine or production batch has the same module and behavior, so treat this as a compatibility issue for this particular hardware setup rather than a claim about every Wujie 15X Pro.

Environment

The main environment where I saw the problem:

  • Laptop: MECHREVO Wujie 15X Pro
  • Linux: CachyOS / Arch Linux
  • KDE Plasma with Wayland
  • Gaze: 0.2.12
  • RGB/IR camera: SunplusIT Inc.
  • USB VID:PID: 2b7e:b663
  • RGB: /dev/video0
  • IR: /dev/video2

List the devices with:

v4l2-ctl --list-devices

On my laptop, the output mapped the camera nodes like this:

HD Webcam: HD Webcam
    /dev/video0
    /dev/video1

HD Webcam: IR Camera
    /dev/video2
    /dev/video3

Inspect the RGB and IR nodes with:

v4l2-ctl -d /dev/video0 --all
v4l2-ctl -d /dev/video2 --all

The RGB node supported MJPG and YUYV. The IR node reported:

Card type        : HD Webcam: IR Camera
Width/Height     : 640/360
Pixel Format     : GREY

Symptoms

Gaze’s configuration and enrollment initially looked normal. Running:

gaze doctor

reported something like:

21 passed, 0 warnings, 0 errors

The face template also contained both RGB and IR data. Right after enrollment, authentication worked:

gaze auth
Face Verified.

After a full reboot, face authentication stopped working everywhere: the login screen, lock screen, sudo, and Gaze GUI.

The logs showed that both cameras opened:

VerifyStart: sensing faces ... run_rgb=true run_ir=true
Attempting to open GStreamer camera: v4l2src device=/dev/video0 ...
Attempting to open GStreamer camera: v4l2src device=/dev/video2 ...
IR stream produced a lit frame: mean_luma=41
VerifyStart: frame budget spent matched=false

That made it easy to investigate the wrong things. Possible explanations included:

  • The template was not saved or reloaded correctly.
  • The Gaze daemon started too early during boot.
  • The PipeWire or WirePlumber session was not ready.
  • The /dev/video* numbering had changed after reboot.
  • The IR emitter was not enabled.
  • USB autosuspend had put the camera into a bad state.

I checked these one by one.

First, rule out PipeWire

My initial Gaze configuration used PipeWire sources:

[cameras]
rgb = "pipewiresrc target-object=...1.0"
ir = "pipewiresrc target-object=...1.2"

The system log also showed that gazed started before the desktop user’s PipeWire service, which made startup timing look suspicious.

To bypass PipeWire completely, I switched to direct V4L2 access:

[cameras]
rgb = "/dev/video0"
ir = "/dev/video2"
emitter_enabled = false
parallel_capture = "never"

On Arch or CachyOS, install gst-plugins-good if v4l2src is missing:

sudo pacman -S gst-plugins-good

Then check that GStreamer can find it:

gst-inspect-1.0 v4l2src | head

After switching to V4L2, both cameras opened consistently. Face recognition still failed after a reboot, so PipeWire was not the root cause.

The key clue: the IR image was much darker

The useful clue was the mean_luma value in the gazed logs.

After reboot, repeated failures showed:

IR mean_luma ≈ 37–46

When face recognition had worked before, the value was much higher:

IR mean_luma ≈ 113–170

One successful authentication looked like this:

RGB face region: mean_luma=100
IR stream produced a lit frame: mean_luma=170
AUTH_EXIT=0

The camera, face template, and Gaze configuration had not changed. The obvious difference was the IR image brightness.

At this point, matched=false looked like a symptom rather than the cause: after a cold boot, the IR camera was not in the same working state as when I enrolled the face template.

Enabling the emitter did not help

Gaze supports this setting:

emitter_enabled = true

It can control the IR emitter on some Windows Hello cameras. With this Sunplus camera, enabling it still produced:

IR stream produced a lit frame: mean_luma=37

That setting did not fix the problem on my laptop, so I turned it back off:

emitter_enabled = false

IR gain was not the problem either

The IR node exposed a gain control:

v4l2-ctl -d /dev/video2 --list-ctrls-menus

Its range was approximately:

gain: min=100 max=1500

I tried values of 100, 300, 500, 800, and 1200. The IR mean_luma stayed around 37–41 and authentication continued to fail. It was not simply a low IR gain setting.

USB autosuspend was not the cause

By default, the camera allowed runtime autosuspend:

/sys/bus/usb/devices/1-1/power/control = auto
runtime_status = suspended
autosuspend_delay_ms = 2000

I tried keeping the IR camera streaming for eight seconds:

timeout 8s gst-launch-1.0 -q \
  v4l2src device=/dev/video2 ! \
  videoconvert ! fakesink

The USB device was active, but face authentication still failed immediately afterwards:

runtime_status = active
IR mean_luma=46
matched=false

Keeping USB awake by itself did not solve it.

The working reproduction: warm up RGB, then authenticate

The breakthrough came when I reviewed a log from a time Gaze GUI had started working again even though I had not changed anything. Just before that, I had opened the Gaze GUI to enroll a face. Its enrollment flow opens the camera for a local preview, then releases it back to the daemon.

I tested this directly by making no configuration changes and streaming from the RGB camera for five seconds:

timeout 5s gst-launch-1.0 -q \
  v4l2src device=/dev/video0 ! \
  videoconvert ! fakesink

Then I immediately ran:

gaze auth

Authentication succeeded. That strongly suggested a hardware initialization problem after the camera’s cold start.

Before the RGB warm-up, the logs showed:

IR mean_luma ≈ 40
AUTH_EXIT=1

Afterward, they showed:

RGB face region: mean_luma=88
IR stream produced a lit frame: mean_luma=117
AUTH_EXIT=0

The behavior was:

Cold boot
  → open IR directly
  → IR is in an abnormal state, brightness around 40
  → Gaze cannot match the face

But:

Cold boot
  → stream RGB for a few seconds
  → the Sunplus camera reaches its normal state
  → open IR
  → IR brightness returns to 100+
  → Gaze authenticates successfully

This also explains why opening Gaze GUI’s enrollment screen seemed to fix the problem by itself: the preview had warmed up the RGB camera.

One warm-up is enough

I tested whether the five-second warm-up had to run before every authentication. After a successful RGB warm-up, I waited until USB runtime status returned to suspended, then ran gaze auth again.

It still succeeded:

AUTH_EXIT=0
IR mean_luma=113

The initialized state did not disappear when USB entered ordinary runtime suspend. For this boot cycle, I only needed to warm up the camera once.

That means I did not need to:

  • Disable USB autosuspend.
  • Add a five-second delay before every face check.
  • Keep the camera streaming continuously.
  • Run a background daemon that holds the camera open.

I only needed one RGB warm-up after each cold boot.

Workaround: warm up RGB before starting Gaze

I would not edit the distribution’s unit file directly:

/usr/lib/systemd/system/gazed.service

A package update could replace it. Instead, create a systemd drop-in:

sudo mkdir -p /etc/systemd/system/gazed.service.d

Create /etc/systemd/system/gazed.service.d/10-camera-warmup.conf with:

[Service]
ExecStartPre=-/usr/bin/timeout 5s /usr/bin/gst-launch-1.0 -q v4l2src device=/dev/video0 ! videoconvert ! fakesink

Then reload systemd and restart Gaze:

sudo systemctl daemon-reload
sudo systemctl restart gazed

Check the resulting unit:

systemctl cat gazed

You should see:

# /etc/systemd/system/gazed.service.d/10-camera-warmup.conf
[Service]
ExecStartPre=-/usr/bin/timeout 5s /usr/bin/gst-launch-1.0 -q v4l2src device=/dev/video0 ! videoconvert ! fakesink

The leading - on ExecStartPre= is intentional. timeout 5s usually exits with a non-zero status when the time is up. The prefix tells systemd to continue starting gazed even if the warm-up command returns that status.

Why run the warm-up before Gaze

On my laptop, the boot log showed this order:

19:03:11.785  uvcvideo found RGB
19:03:11.917  uvcvideo found IR
19:03:12.697  gazed started

So /dev/video0 already existed when ExecStartPre ran.

The startup sequence becomes:

uvcvideo enumerates RGB and IR
          ↓
systemd prepares to start gazed
          ↓
ExecStartPre streams from /dev/video0 for about five seconds
          ↓
the Sunplus camera completes its cold-start initialization
          ↓
gazed starts
          ↓
SDDM, KDE, and sudo can use Gaze

This is more specific than delaying gazed for five seconds and hoping the problem goes away. It actively streams from the RGB sensor to initialize the camera.

My Gaze camera settings

I kept these settings:

[cameras]
rgb = "/dev/video0"
ir = "/dev/video2"
emitter_enabled = false
parallel_capture = "never"

Your device numbers may differ. Use v4l2-ctl --list-devices to identify the RGB and IR nodes before copying the configuration.

Check whether it worked

Without rebooting, I could first restart Gaze and test authentication:

sudo systemctl restart gazed
sleep 1
gaze auth

After the warm-up, my logs showed:

RGB face region: mean_luma=100
IR stream produced a lit frame: mean_luma=170

Authentication succeeded with:

AUTH_EXIT=0

To test the cold-start issue properly, reboot the machine and try face recognition at the SDDM login screen, KDE lock screen, or with gaze auth.

Summary

This was confusing because every component looked healthy: Gaze could open both cameras, IR produced an image, the template existed, PAM and D-Bus worked, and gaze doctor passed. Authentication still ended in matched=false.

The likely cause was the camera module itself. On my MECHREVO Wujie 15X Pro, the SunplusIT 2b7e:b663 RGB/IR camera needed the RGB sensor to stream after a cold boot before the IR camera entered the same working state as during enrollment.

The workaround is simple:

On each boot
  → stream from RGB for five seconds before gazed starts
  → use face recognition normally for the rest of that boot session

If Gaze, uvcvideo, or the Linux kernel eventually adds the correct initialization quirk for this camera, I can remove the drop-in. Until then, a short RGB warm-up at boot is cleaner for my setup than disabling autosuspend, keeping the camera busy, or giving up on IR authentication.