Skip to content

Commit 7cc2ba1

Browse files
committed
fix: lower detection threshold and improve IR camera timing for Lenovo
- Lower DEFAULT_DETECTION_THRESHOLD from 0.9 to 0.6 - Increase IR_TRIGGER_DELAY_MS from 200ms to 1500ms - Increase CAMERA_WARMUP_FRAMES (10->15) and CAMERA_WARMUP_DELAY_MS (100->200ms) - Always save failed enrollment frame with brightness and score info - Add YuNet face score logging to help diagnose threshold issues - Add docs/LENOVO_IR_DEBUG.md with full troubleshooting guide Tested on Lenovo with Ubuntu 26.04, /dev/video2 IR camera.
1 parent 99731eb commit 7cc2ba1

6 files changed

Lines changed: 395 additions & 20 deletions

File tree

LENOVO_IR_DEBUG.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Lenovo IR Camera — Debug Guide
2+
3+
## Symptom
4+
5+
```
6+
Response: ENROLL_FAIL Found 0 faces in ir. Expecting exactly 1.
7+
```
8+
9+
## Root Cause
10+
11+
The YuNet face detector assigns **significantly lower confidence scores** to faces
12+
captured by IR cameras (grayscale image, low contrast, often overexposed).
13+
14+
The default threshold of `0.9` was tuned for standard RGB webcams. On IR cameras
15+
it is nearly impossible to reach. Lowering it to `0.5–0.6` resolves the issue
16+
in most cases.
17+
18+
---
19+
20+
## Quick Fix (no recompilation required)
21+
22+
Edit `/etc/linuxcampam/config.ini`:
23+
24+
```ini
25+
[Auth]
26+
detection_threshold = 0.5
27+
28+
[Capture]
29+
enroll_averaging = on
30+
enroll_average_frames = 7
31+
```
32+
33+
Then restart the daemon:
34+
```bash
35+
sudo systemctl restart linuxcampam
36+
```
37+
38+
---
39+
40+
## Diagnosing with logs
41+
42+
During enrollment, watch the logs in real time:
43+
44+
```bash
45+
journalctl -u linuxcampam -f
46+
```
47+
48+
What to look for:
49+
50+
| Log message | Meaning |
51+
|-------------|---------|
52+
| `0 faces found above threshold (0.9)` | Threshold too high → lower it |
53+
| `Best score: 0.65 \| Threshold: 0.9` | Face found but below threshold → lower to 0.55 |
54+
| `Brightness: 12` | Frame nearly black → IR emitter not activating |
55+
| `Brightness: 240` | Frame overexposed → camera exposure issue |
56+
57+
---
58+
59+
## Inspecting the failed frame
60+
61+
On every failed enrollment the captured frame is automatically saved to:
62+
63+
```
64+
/var/log/linuxcampam/failed_enroll_ir_<username>.jpg
65+
```
66+
67+
Open it to check what the camera actually captured:
68+
```bash
69+
xdg-open /var/log/linuxcampam/failed_enroll_ir_<username>.jpg
70+
```
71+
72+
- **Nearly black frame** → IR emitter did not activate (see section below)
73+
- **Grainy / blurry frame** → increase `enroll_average_frames`
74+
- **Face visible but not detected** → lower `detection_threshold` further
75+
- **No face in frame** → positioning issue during enrollment
76+
77+
---
78+
79+
## IR emitter not activating
80+
81+
If the saved frame is nearly black:
82+
83+
1. Verify `linux-enable-ir-emitter` is installed:
84+
```bash
85+
ls /usr/local/bin/linux-enable-ir-emitter
86+
```
87+
88+
2. Test it manually:
89+
```bash
90+
sudo linux-enable-ir-emitter run
91+
```
92+
93+
3. If not installed:
94+
```bash
95+
sudo apt install linux-enable-ir-emitter
96+
# or from source: https://github.com/EmixamPP/linux-enable-ir-emitter
97+
```
98+
99+
4. Configure for your specific hardware model:
100+
```bash
101+
sudo linux-enable-ir-emitter configure
102+
```
103+
Follow the interactive procedure — move your head in front of the camera
104+
while it tries different configurations. When the emitter blinks and you see
105+
`The infrared emitter has been successfully enabled!` you are done.
106+
107+
---
108+
109+
## Finding the correct IR camera device
110+
111+
```bash
112+
# List all webcams
113+
v4l2-ctl --list-devices
114+
115+
# Identify the IR camera by checking shape and brightness
116+
python3 -c "
117+
import cv2
118+
for i in range(4):
119+
cap = cv2.VideoCapture(i)
120+
if not cap.isOpened():
121+
print(f'video{i}: could not open')
122+
continue
123+
ret, frame = cap.read()
124+
cap.release()
125+
if ret:
126+
print(f'video{i}: shape={frame.shape} brightness={frame.mean():.0f}')
127+
"
128+
```
129+
130+
The IR camera has **1 channel** (shape like `(360, 640)`) instead of 3 (RGB).
131+
On Lenovo laptops it is typically `/dev/video2`.
132+
133+
---
134+
135+
## Recommended settings for Lenovo ThinkPad / IdeaPad
136+
137+
```ini
138+
[Auth]
139+
detection_threshold = 0.5
140+
timeout_ms = 5000
141+
142+
[Capture]
143+
enroll_hdr = off ; IR cameras do not support HDR
144+
enroll_averaging = on
145+
enroll_average_frames = 7
146+
147+
[Hardware]
148+
camera_path_ir = /dev/video2 ; verify with v4l2-ctl --list-devices
149+
```
150+
151+
---
152+
153+
## Code changes (this fix)
154+
155+
| File | Change |
156+
|------|--------|
157+
| `include/constants.hpp` | `IR_TRIGGER_DELAY_MS`: 200 → 1500 ms |
158+
| `include/constants.hpp` | `CAMERA_WARMUP_FRAMES`: 10 → 15 |
159+
| `include/constants.hpp` | `CAMERA_WARMUP_DELAY_MS`: 100 → 200 ms |
160+
| `src/service/config.hpp` | `DEFAULT_DETECTION_THRESHOLD`: 0.9 → 0.6 |
161+
| `src/service/auth_engine.cpp` | Log YuNet score + brightness in `generateEmbedding` |
162+
| `src/service/auth_engine.cpp` | Always save failed enrollment frame with diagnostic info |
163+
| `config/config.ini` | Updated default `detection_threshold` + averaging enabled |

config/config.ini

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ ir_emitter_path = /usr/local/bin/linux-enable-ir-emitter
2828
; How confident the AI must be that it found a face.
2929
; High values (0.9) are safe but fail in poor light/IR.
3030
; Lower values (0.5-0.6) improve IR success but risk false detections.
31-
detection_threshold = 0.9
31+
; Lowered from 0.9 to 0.6: IR cameras (e.g. Lenovo, Dell) produce grayscale,
32+
; low-contrast images where YuNet rarely scores above 0.7.
33+
; Use 0.5-0.6 for IR cameras, 0.8-0.9 for high-quality RGB cameras only.
34+
detection_threshold = 0.6
3235

3336
; Timeout in milliseconds to wait for a successful match.
3437
; timeout_ms = 3000
@@ -59,8 +62,9 @@ detection_threshold = 0.9
5962
; Enhanced capture settings for enrollment quality.
6063
; HDR uses multiple exposures if camera supports manual exposure control.
6164
; enroll_hdr = auto ; auto | on | off
62-
; enroll_averaging = on ; on | off
63-
; enroll_average_frames = 5
65+
; For IR cameras (e.g. Lenovo, Dell), enable averaging to reduce noise:
66+
enroll_averaging = on
67+
enroll_average_frames = 7
6468

6569
; Verification capture (speed-focused, defaults to fast single-frame).
6670
; verify_averaging = off

docs/LENOVO_IR_DEBUG.md

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Lenovo IR Camera — Debug Guide
2+
3+
## Symptom
4+
5+
```
6+
Response: ENROLL_FAIL Found 0 faces in ir. Expecting exactly 1.
7+
```
8+
9+
## Root Cause
10+
11+
The YuNet face detector assigns **significantly lower confidence scores** to faces
12+
captured by IR cameras (grayscale image, low contrast, often overexposed).
13+
14+
The default threshold of `0.9` was tuned for standard RGB webcams. On IR cameras
15+
it is nearly impossible to reach. Lowering it to `0.5–0.6` resolves the issue
16+
in most cases.
17+
18+
---
19+
20+
## Quick Fix (no recompilation required)
21+
22+
Edit `/etc/linuxcampam/config.ini`:
23+
24+
```ini
25+
[Auth]
26+
detection_threshold = 0.5
27+
28+
[Capture]
29+
enroll_averaging = on
30+
enroll_average_frames = 7
31+
```
32+
33+
Then restart the daemon:
34+
```bash
35+
sudo systemctl restart linuxcampam
36+
```
37+
38+
---
39+
40+
## Diagnosing with logs
41+
42+
During enrollment, watch the logs in real time:
43+
44+
```bash
45+
journalctl -u linuxcampam -f
46+
```
47+
48+
What to look for:
49+
50+
| Log message | Meaning |
51+
|-------------|---------|
52+
| `0 faces found above threshold (0.9)` | Threshold too high → lower it |
53+
| `Best score: 0.65 \| Threshold: 0.9` | Face found but below threshold → lower to 0.55 |
54+
| `Brightness: 12` | Frame nearly black → IR emitter not activating |
55+
| `Brightness: 240` | Frame overexposed → camera exposure issue |
56+
57+
---
58+
59+
## Inspecting the failed frame
60+
61+
On every failed enrollment the captured frame is automatically saved to:
62+
63+
```
64+
/var/log/linuxcampam/failed_enroll_ir_<username>.jpg
65+
```
66+
67+
Open it to check what the camera actually captured:
68+
```bash
69+
xdg-open /var/log/linuxcampam/failed_enroll_ir_<username>.jpg
70+
```
71+
72+
- **Nearly black frame** → IR emitter did not activate (see section below)
73+
- **Grainy / blurry frame** → increase `enroll_average_frames`
74+
- **Face visible but not detected** → lower `detection_threshold` further
75+
- **No face in frame** → positioning issue during enrollment
76+
77+
---
78+
79+
## IR emitter not activating
80+
81+
If the saved frame is nearly black:
82+
83+
1. Verify `linux-enable-ir-emitter` is installed:
84+
```bash
85+
ls /usr/local/bin/linux-enable-ir-emitter
86+
```
87+
88+
2. Test it manually:
89+
```bash
90+
sudo linux-enable-ir-emitter run
91+
```
92+
93+
3. If not installed:
94+
```bash
95+
sudo apt install linux-enable-ir-emitter
96+
# or from source: https://github.com/EmixamPP/linux-enable-ir-emitter
97+
```
98+
99+
4. Configure for your specific hardware model:
100+
```bash
101+
sudo linux-enable-ir-emitter configure
102+
```
103+
Follow the interactive procedure — move your head in front of the camera
104+
while it tries different configurations. When the emitter blinks and you see
105+
`The infrared emitter has been successfully enabled!` you are done.
106+
107+
---
108+
109+
## Finding the correct IR camera device
110+
111+
```bash
112+
# List all webcams
113+
v4l2-ctl --list-devices
114+
115+
# Identify the IR camera by checking shape and brightness
116+
python3 -c "
117+
import cv2
118+
for i in range(4):
119+
cap = cv2.VideoCapture(i)
120+
if not cap.isOpened():
121+
print(f'video{i}: could not open')
122+
continue
123+
ret, frame = cap.read()
124+
cap.release()
125+
if ret:
126+
print(f'video{i}: shape={frame.shape} brightness={frame.mean():.0f}')
127+
"
128+
```
129+
130+
The IR camera has **1 channel** (shape like `(360, 640)`) instead of 3 (RGB).
131+
On Lenovo laptops it is typically `/dev/video2`.
132+
133+
---
134+
135+
## Recommended settings for Lenovo ThinkPad / IdeaPad
136+
137+
```ini
138+
[Auth]
139+
detection_threshold = 0.5
140+
timeout_ms = 5000
141+
142+
[Capture]
143+
enroll_hdr = off ; IR cameras do not support HDR
144+
enroll_averaging = on
145+
enroll_average_frames = 7
146+
147+
[Hardware]
148+
camera_path_ir = /dev/video2 ; verify with v4l2-ctl --list-devices
149+
```
150+
151+
---
152+
153+
## Code changes (this fix)
154+
155+
| File | Change |
156+
|------|--------|
157+
| `include/constants.hpp` | `IR_TRIGGER_DELAY_MS`: 200 → 1500 ms |
158+
| `include/constants.hpp` | `CAMERA_WARMUP_FRAMES`: 10 → 15 |
159+
| `include/constants.hpp` | `CAMERA_WARMUP_DELAY_MS`: 100 → 200 ms |
160+
| `src/service/config.hpp` | `DEFAULT_DETECTION_THRESHOLD`: 0.9 → 0.6 |
161+
| `src/service/auth_engine.cpp` | Log YuNet score + brightness in `generateEmbedding` |
162+
| `src/service/auth_engine.cpp` | Always save failed enrollment frame with diagnostic info |
163+
| `config/config.ini` | Updated default `detection_threshold` + averaging enabled |

include/constants.hpp

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,18 @@ inline constexpr double RGB_CHANNELS = 3.0;
1818
inline constexpr uid_t DEFAULT_MIN_UID = 1000;
1919

2020
// Camera & Auth constants
21-
inline constexpr int CAMERA_WARMUP_FRAMES = 10;
22-
inline constexpr int CAMERA_WARMUP_DELAY_MS = 100;
21+
// WARMUP_FRAMES and WARMUP_DELAY_MS increased from 10/100 to 15/200:
22+
// IR cameras (e.g. Lenovo) need more frames discarded and longer settle time
23+
// before the auto-exposure stabilizes and produces a usable image.
24+
inline constexpr int CAMERA_WARMUP_FRAMES = 15;
25+
inline constexpr int CAMERA_WARMUP_DELAY_MS = 200;
2326
inline constexpr int CAMERA_AVERAGE_FRAMES = 5;
24-
inline constexpr int IR_TRIGGER_DELAY_MS = 200;
27+
28+
// IR_TRIGGER_DELAY_MS increased from 200ms to 1500ms:
29+
// On Lenovo laptops the IR emitter hardware needs significantly more time
30+
// to activate after being triggered before the sensor receives usable IR light.
31+
// 200ms was too short, resulting in near-black frames (brightness ~14/255).
32+
inline constexpr int IR_TRIGGER_DELAY_MS = 1500;
2533
inline constexpr int CAPTURE_RETRY_DELAY_S = 1;
2634

2735
// HDR Constants
@@ -36,5 +44,5 @@ inline constexpr int CAPTURE_RETRY_ATTEMPTS = 3;
3644

3745
inline constexpr float MIRROR_THRESHOLD_DEFAULT = 0.6f; // detection confidence
3846
inline constexpr int MIRROR_SIZE = 640;
39-
inline constexpr int MIRROR_NMS = 5000; // keep top K bboxes before NMS .
40-
} // namespace linuxcampam
47+
inline constexpr int MIRROR_NMS = 5000; // keep top K bboxes before NMS
48+
} // namespace linuxcampam

0 commit comments

Comments
 (0)