Spaces:
Running
Tighten recognition: raise threshold to 0.50, add 2-hit debounce
Browse filesTwo fixes for "always recognises the same person":
1. Threshold 0.35 → 0.50
MobileFaceNet embeddings for different people typically score 0.20–0.45;
the same person scores 0.55–0.85. The old threshold of 0.35 sits right
in the overlap zone, so random faces exceeded it. 0.50 sits safely
above the inter-person range while remaining reachable for the same
person.
2. 2-hit recognition debounce (RECOGNITION_DEBOUNCE = 2)
The same name must appear in two consecutive 0.5-second frames before
GREETING is triggered. A single ambiguous frame (slight pose change,
partial occlusion) can no longer cause a false positive. The counter
resets on every mismatch, NoFaceDetected, wake-up, or state change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- recognizer/face_db.py +1 -1
- recognizer/main.py +32 -11
|
@@ -214,7 +214,7 @@ def wipe() -> None:
|
|
| 214 |
def find_match(
|
| 215 |
frame_bgr: np.ndarray,
|
| 216 |
db: dict[str, list[list[float]]],
|
| 217 |
-
threshold: float = 0.
|
| 218 |
) -> Optional[str]:
|
| 219 |
"""Return matched name if recognised, None if face present but unknown.
|
| 220 |
|
|
|
|
| 214 |
def find_match(
|
| 215 |
frame_bgr: np.ndarray,
|
| 216 |
db: dict[str, list[list[float]]],
|
| 217 |
+
threshold: float = 0.50,
|
| 218 |
) -> Optional[str]:
|
| 219 |
"""Return matched name if recognised, None if face present but unknown.
|
| 220 |
|
|
@@ -23,10 +23,11 @@ from recognizer.tts import speak
|
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
| 25 |
|
| 26 |
-
ACTIVE_TIMEOUT
|
| 27 |
-
DOA_DEBOUNCE
|
| 28 |
-
FACE_INTERVAL
|
| 29 |
-
SCAN_AMPLITUDE
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
class State(Enum):
|
|
@@ -93,7 +94,9 @@ class Recognizer(ReachyMiniApp):
|
|
| 93 |
last_face_check = 0.0
|
| 94 |
enrollment_frames: list[np.ndarray] = []
|
| 95 |
greeting_name = ""
|
| 96 |
-
|
|
|
|
|
|
|
| 97 |
|
| 98 |
reachy_mini.goto_sleep()
|
| 99 |
|
|
@@ -134,6 +137,8 @@ class Recognizer(ReachyMiniApp):
|
|
| 134 |
scan_t0 = active_start
|
| 135 |
last_face_check = 0.0
|
| 136 |
enrollment_frames.clear()
|
|
|
|
|
|
|
| 137 |
state = State.ACTIVE
|
| 138 |
|
| 139 |
# ---------- ACTIVE ----------
|
|
@@ -157,12 +162,27 @@ class Recognizer(ReachyMiniApp):
|
|
| 157 |
name = find_match(frame, face_db)
|
| 158 |
jpeg = get_face_jpeg(frame)
|
| 159 |
if name:
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
else:
|
|
|
|
|
|
|
|
|
|
| 166 |
speak(
|
| 167 |
"I don't know you yet. "
|
| 168 |
"Please enter your name on the control panel.",
|
|
@@ -174,7 +194,8 @@ class Recognizer(ReachyMiniApp):
|
|
| 174 |
_shared["snapshot"] = jpeg
|
| 175 |
state = State.ENROLLING
|
| 176 |
except NoFaceDetected:
|
| 177 |
-
|
|
|
|
| 178 |
|
| 179 |
# Timeout: nobody showed up
|
| 180 |
if state == State.ACTIVE and time.time() - active_start > ACTIVE_TIMEOUT:
|
|
|
|
| 23 |
|
| 24 |
logger = logging.getLogger(__name__)
|
| 25 |
|
| 26 |
+
ACTIVE_TIMEOUT = 15.0 # seconds before returning to sleep with no face
|
| 27 |
+
DOA_DEBOUNCE = 3 # consecutive speech-detected readings to wake up
|
| 28 |
+
FACE_INTERVAL = 0.5 # seconds between face-recognition attempts
|
| 29 |
+
SCAN_AMPLITUDE = 0.35 # head scan amplitude (world-y, metres) during ACTIVE
|
| 30 |
+
RECOGNITION_DEBOUNCE = 2 # consecutive same-name hits required to confirm identity
|
| 31 |
|
| 32 |
|
| 33 |
class State(Enum):
|
|
|
|
| 94 |
last_face_check = 0.0
|
| 95 |
enrollment_frames: list[np.ndarray] = []
|
| 96 |
greeting_name = ""
|
| 97 |
+
rec_candidate = "" # name accumulating consecutive hits
|
| 98 |
+
rec_hits = 0 # consecutive hit count for rec_candidate
|
| 99 |
+
scan_t0 = 0.0 # reference time for head-scan idle animation
|
| 100 |
|
| 101 |
reachy_mini.goto_sleep()
|
| 102 |
|
|
|
|
| 137 |
scan_t0 = active_start
|
| 138 |
last_face_check = 0.0
|
| 139 |
enrollment_frames.clear()
|
| 140 |
+
rec_candidate = ""
|
| 141 |
+
rec_hits = 0
|
| 142 |
state = State.ACTIVE
|
| 143 |
|
| 144 |
# ---------- ACTIVE ----------
|
|
|
|
| 162 |
name = find_match(frame, face_db)
|
| 163 |
jpeg = get_face_jpeg(frame)
|
| 164 |
if name:
|
| 165 |
+
# Debounce: require RECOGNITION_DEBOUNCE consecutive
|
| 166 |
+
# hits of the same name before confirming.
|
| 167 |
+
if name == rec_candidate:
|
| 168 |
+
rec_hits += 1
|
| 169 |
+
else:
|
| 170 |
+
rec_candidate = name
|
| 171 |
+
rec_hits = 1
|
| 172 |
+
logger.debug("Recognition hit %d/%d: %s",
|
| 173 |
+
rec_hits, RECOGNITION_DEBOUNCE, name)
|
| 174 |
+
if rec_hits >= RECOGNITION_DEBOUNCE:
|
| 175 |
+
rec_candidate = ""
|
| 176 |
+
rec_hits = 0
|
| 177 |
+
greeting_name = name
|
| 178 |
+
with _lock:
|
| 179 |
+
_shared["recognized_name"] = name
|
| 180 |
+
_shared["snapshot"] = jpeg
|
| 181 |
+
state = State.GREETING
|
| 182 |
else:
|
| 183 |
+
# Unknown face — reset debounce and enroll
|
| 184 |
+
rec_candidate = ""
|
| 185 |
+
rec_hits = 0
|
| 186 |
speak(
|
| 187 |
"I don't know you yet. "
|
| 188 |
"Please enter your name on the control panel.",
|
|
|
|
| 194 |
_shared["snapshot"] = jpeg
|
| 195 |
state = State.ENROLLING
|
| 196 |
except NoFaceDetected:
|
| 197 |
+
rec_candidate = ""
|
| 198 |
+
rec_hits = 0
|
| 199 |
|
| 200 |
# Timeout: nobody showed up
|
| 201 |
if state == State.ACTIVE and time.time() - active_start > ACTIVE_TIMEOUT:
|