无界 15X Pro 在 Linux 下 Gaze 人脸识别重启后失效:Sunplus RGB/IR 摄像头冷启动预热方案

笔记

本文由 AI 根据我的实际排查过程、系统日志和实机测试整理生成,技术结论与命令均来自本次实际故障排查。

最近在 无界 15X Pro 上配置 Linux 人脸识别时,碰到了一个很奇怪的问题:Gaze、PAM、RGB 摄像头、IR 摄像头和人脸模板看起来都正常,刚配置好时也能刷脸,但只要整机重启,Gaze GUI、sudo、KDE 锁屏和登录界面就会一起无法识别人脸。

摄像头本身又没有直接报错:RGB 能打开,IR 能打开,日志里也有红外画面,但最后始终是 matched=false。

一路排查后发现,问题并不在 Gaze 模型、PAM、PipeWire、模板文件,也不在 USB autosuspend,而很可能是无界 15X Pro 上这颗 SunplusIT RGB + IR 复合摄像头的冷启动初始化问题:整机冷启动以后,需要先让 RGB 摄像头真正工作几秒,IR 才会进入正常状态。

至于鸡哥到底从什么犄角旮旯找来的这块摄像头模组,我不清楚;但至少在 Linux 下,它确实需要额外伺候一下。

这篇文章主要记录完整排查过程和最终 workaround,方便以后自己重装系统时直接抄作业,也给碰到同款机器、同款摄像头的人一个参考。

说明:目前能够确认这个问题在我的无界 15X Pro 上稳定复现。是否所有无界 15X Pro 都使用完全相同的摄像头模组、是否所有批次都会出现同样行为,我没有足够样本,因此本文把它当作特定硬件组合的兼容性问题,而不是直接断言所有同型号机器都有这个 bug。

环境

出问题时的主要环境:

  • 机器:无界 15X Pro
  • Linux:CachyOS / Arch 系
  • KDE Plasma + Wayland
  • Gaze:0.2.12
  • RGB/IR 摄像头:SunplusIT Inc
  • USB VID:PID:2b7e:b663
  • RGB:/dev/video0
  • IR:/dev/video2

用下面的命令可以看到设备:

v4l2-ctl --list-devices

本机输出对应关系为:

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

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

进一步检查:

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

RGB 节点支持 MJPG / YUYV,IR 节点则是:

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

症状

Gaze 的配置和录入都看起来正常:

gaze doctor

可以得到类似:

21 passed, 0 warnings, 0 errors

人脸模板也同时包含 RGB 和 IR。

刚录入完成时:

gaze auth

可以正常:

Face Verified.

但是整机重启以后,登录、锁屏、sudo、Gaze GUI 全部开始失败。

日志里摄像头其实都能正常打开:

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

这个现象一开始很容易把排查方向带偏,可能的原因包括:

  • 模板保存后重新加载坏了;
  • Gaze daemon 开机启动太早;
  • PipeWire / WirePlumber 会话没有准备好;
  • /dev/video* 重启后换号;
  • IR emitter 没打开;
  • USB autosuspend 把摄像头搞坏了。

这些方向后来基本都逐一排查过。

先排除 PipeWire

最早我的 Gaze 使用 PipeWire source:

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

系统日志里又刚好能看到 gazed 比桌面用户的 PipeWire 更早启动,因此最开始很容易怀疑是启动时序问题。

为了完全绕开 PipeWire,我最后改成直接访问 V4L2:

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

Arch / CachyOS 上如果缺少 v4l2src,还需要安装:

sudo pacman -S gst-plugins-good

检查:

gst-inspect-1.0 v4l2src | head

切到 V4L2 后,RGB 和 IR 都能稳定打开,但整机重启后仍然无法识别。

结果切换以后问题依旧存在,因此 PipeWire 不是最终根因。

最关键的线索:IR 亮度完全不同

真正有价值的线索来自 gazed 日志里的 mean_luma。

重启后连续失败时:

IR mean_luma ≈ 37 ~ 46

而之前能够正常刷脸的时候:

IR mean_luma ≈ 113 ~ 170

例如一次明确成功的认证:

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

摄像头、模板和 Gaze 配置都没有变化,唯一明显不同的是 IR 画面亮度。

这时候基本可以确认,matched=false 只是最终表现:冷启动后的 IR 摄像头并没有进入录入模板时的正常工作状态。

emitter_enabled 没有解决

Gaze 支持:

emitter_enabled = true

理论上可以主动控制部分 Windows Hello 摄像头的 IR emitter。

但在这颗 2b7e:b663 Sunplus 摄像头上,打开它以后仍然只有:

IR stream produced a lit frame: mean_luma=37

所以至少在我这台机器上,这个开关没有解决问题。

最后还是恢复:

emitter_enabled = false

IR gain 也不是问题

IR 节点暴露了 gain:

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

范围大约是:

gain: min=100 max=1500

我依次测试了:

100
300
500
800
1200

结果 IR mean_luma 依旧只有 37~41,全部识别失败。

所以它也不是一个简单的 IR 增益过低问题。

USB autosuspend 也被排除了

摄像头默认允许 runtime autosuspend:

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

我尝试让 IR 摄像头保持连续工作 8 秒:

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

此时 USB 已经是:

runtime_status = active

但紧接着人脸认证仍然失败:

IR mean_luma=46
matched=false

因此仅仅让 USB 保持唤醒并不能解决问题。

真正的复现:先预热 RGB,马上恢复

真正找到突破口,是因为我回看了一次“什么都没改,但 Gaze GUI 突然又能验证”的日志。

时间线上刚好发生了一个重要动作:我打开 Gaze GUI 准备新增人脸。Gaze GUI 的 enrollment 流程会先打开摄像头做本地预览,然后再把摄像头释放给 daemon。

于是我单独做了一个实验:什么都不改,只让 RGB 摄像头先跑 5 秒。

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

然后立刻:

gaze auth

结果认证立即成功。到这里基本可以确认,问题确实和摄像头冷启动后的硬件状态有关。

日志从之前的:

IR mean_luma ≈ 40
AUTH_EXIT=1

变成:

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

也就是说:

冷启动
  ↓
直接开 IR
  ↓
IR 状态异常,亮度约 40
  ↓
Gaze 无法匹配

而:

冷启动
  ↓
先让 RGB 真正 stream 几秒
  ↓
Sunplus 复合摄像头进入正确状态
  ↓
再开 IR
  ↓
IR 亮度恢复 100+
  ↓
Gaze 正常认证

这也解释了之前那个看起来很奇怪的现象:打开 Gaze GUI 的录入页面后,问题会像“自己好了”一样消失。实际上不是玄学,而是 GUI 的录入流程顺手完成了 RGB 预热。

预热一次就够了

为了确认是不是每次认证前都要等 5 秒,我又做了一个测试。

RGB 预热成功以后,等待 USB 再次进入:

runtime_status = suspended

然后再次执行 gaze auth。

结果依然:

AUTH_EXIT=0
IR mean_luma=113

这说明这个初始化状态不会因为普通 runtime suspend 再次丢失。一次预热后,至少这一轮开机周期内都能保持正常。

这意味着不需要:

  • 禁用 USB autosuspend;
  • 每次刷脸前延迟 5 秒;
  • 让摄像头一直常亮;
  • 写一个后台守护进程不停占用摄像头。

只需要每次整机冷启动时预热 RGB 一次。

最终 workaround:在 gazed 启动前预热 RGB 5 秒

这里不建议直接修改发行版提供的:

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

因为包更新会覆盖它。

使用 systemd drop-in:

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

新建:

/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

然后:

sudo systemctl daemon-reload
sudo systemctl restart gazed

检查:

systemctl cat gazed

应该能看到:

# /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

这里 ExecStartPre= 前面的 - 是故意的。

timeout 5s 到时间后通常以非 0 状态结束;前面的 - 告诉 systemd 即使预热命令因为 timeout 返回非 0,也继续启动 gazed。

为什么把预热放在 gazed 前面

本机启动日志中,摄像头枚举顺序为:

19:03:11.785  uvcvideo 找到 RGB
19:03:11.917  uvcvideo 找到 IR
19:03:12.697  gazed 启动

所以当 ExecStartPre 开始时,/dev/video0 已经存在。

最终启动流程就变成:

uvcvideo 枚举 RGB / IR
          ↓
准备启动 gazed
          ↓
ExecStartPre 打开 /dev/video0 约 5 秒
          ↓
Sunplus 摄像头完成冷启动初始化
          ↓
gazed 正式启动
          ↓
SDDM / KDE / sudo 调用 Gaze
          ↓
RGB + IR 正常认证

相比单纯延迟整个 gazed 的启动,这个写法更明确:不是“等 5 秒看看会不会好”,而是主动让 RGB sensor 工作,把摄像头初始化到正确状态。

当前 Gaze 摄像头配置

最终保留的相关配置为:

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

如果你的 /dev/video* 对应关系不同,请不要直接照抄编号,先用:

v4l2-ctl --list-devices

确认 RGB 和 IR 节点。

验证修复是否生效

不重启机器时,可以先:

sudo systemctl restart gazed
sleep 1
gaze auth

在我的机器上,应用预热后重新启动 Gaze,日志为:

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

认证成功:

AUTH_EXIT=0

真正要验证冷启动问题,最终还是应该整机 reboot 后在 SDDM / KDE 锁屏或 gaze auth 中测试。

总结

这个问题最麻烦的地方在于,所有表象看起来都很像软件问题:

Gaze 能打开摄像头
IR 也有画面
模板也存在
PAM 正常
DBus 正常
doctor 全绿
但就是 matched=false

结果最后兜了一圈,问题还是回到了这颗摄像头模组本身:

无界 15X Pro 上这颗 SunplusIT 2b7e:b663 RGB/IR 复合摄像头,在 Linux 冷启动后需要先让 RGB sensor stream 一段时间,IR 才进入和正常录入状态一致的工作模式。

最终 workaround 非常简单:

每次开机
→ gazed 启动前
→ RGB 预热 5 秒
→ 后续整次开机周期内正常使用

如果以后 Gaze、uvcvideo 或内核针对这颗摄像头补上正确的初始化 quirk,这个 drop-in 就可以删除。在那之前,开机时让 RGB 预热 5 秒,至少比关闭 autosuspend、长期占用摄像头或者放弃 IR 验证更干净。