Hold, do not tap -- and stop spending the verdict on a frame that cannot carry it
Jorijn worked out the technique and it changes what every number in this project means: "press and LIFT (quick tap) is wrong, holding the sensor until it gives the result is a 100% success rate." The logs agree, on a properly controlled comparison. Same template, same session, learning off for all four blocks, only the technique differing: tapped 4/15 and 3/15, held 15/15 and 15/15. The frame data says why. Over every frame this project has a verdict for, split at 2.5x the idle floor: full contact, interrupt settled 78/175 = 45% match full contact, interrupt asserted 28/142 = 20% partial, interrupt settled 1/9 = 11% partial, interrupt asserted 0/53 = 0% A tap is caught while the finger is still arriving or already leaving. Such a frame is not a hard verdict waiting to happen, it is a wasted one: with the rescan budget at 0 every frame is terminal, so its rejection ends the press. 62 partial frames produced exactly one match between them. So the tracker becomes a Schmitt trigger. A press now STARTS on settled contact and ENDS on the finger leaving, which means a frame taken mid-landing produces no event at all rather than a false rejection. A press that never settles simply yields no verdict and the loop waits for the next one, which is an honest try again. Enrolment is untouched: it passes one threshold for both and keeps its own sample-quality gate inside the trustlet. fptrial.sh now says hold, and defaults to fifteen presses. Instructing a tap for its whole life is what quietly made every rate this project has quoted a worst case, and a tap is not a case the product has -- nobody taps a phone sensor and walks away, they rest a finger until it unlocks.
This commit is contained in:
parent
f3da01a511
commit
284282350b
3 changed files with 80 additions and 11 deletions
|
|
@ -1189,8 +1189,9 @@ public:
|
|||
std::println(std::cerr, "baseline did not calibrate (floor stayed 0)");
|
||||
return false;
|
||||
}
|
||||
std::println("idle floor = {}, finger threshold = {}", baseline_.Floor(),
|
||||
baseline_.Threshold());
|
||||
std::println("idle floor = {}, finger threshold = {}, settled threshold = {}",
|
||||
baseline_.Floor(), baseline_.Threshold(),
|
||||
baseline_.SettledThreshold());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1373,6 +1374,10 @@ public:
|
|||
std::uint32_t fid = 0;
|
||||
int presses = 0;
|
||||
int frames = 0;
|
||||
// Frames that showed a finger arriving but were not settled enough to
|
||||
// spend a verdict on. A press made only of these is a "try again", not
|
||||
// a rejection.
|
||||
int skippedUnsettled = 0;
|
||||
};
|
||||
|
||||
// `accept` is the set of fids that count as a match for THIS request. The
|
||||
|
|
@ -1404,7 +1409,7 @@ public:
|
|||
en::TouchTracker tracker;
|
||||
harvested_ = 0; // no fold has happened in this session yet
|
||||
bool inPress = false, pressMatched = false, pressRejected = false;
|
||||
int pressFrames = 0, rescans = 0;
|
||||
int pressFrames = 0, rescans = 0, skipped = 0;
|
||||
std::uint32_t pressFid = 0;
|
||||
auto t0 = std::chrono::steady_clock::now();
|
||||
auto msSince = [&](auto t) {
|
||||
|
|
@ -1424,8 +1429,11 @@ public:
|
|||
ta::BuildCapturePayload(cap);
|
||||
auto c = SendCommand(app_, ta::Cmd::CaptureImage, cap);
|
||||
bool finger = baseline_.IsFinger(c.metric);
|
||||
// A press begins only once contact is settled; see TouchTracker.
|
||||
bool settled = baseline_.IsSettled(c.metric);
|
||||
fingerPresent_.store(finger);
|
||||
out.frames++;
|
||||
if (finger && !settled && !tracker.FingerDown()) skipped++;
|
||||
|
||||
std::string note;
|
||||
// No recapture on the rising edge. It was tried, on the theory that
|
||||
|
|
@ -1435,7 +1443,7 @@ public:
|
|||
// still flagged FINGER from the first capture) and an empty image
|
||||
// was reported to the matcher. A guaranteed miss on exactly the
|
||||
// case it was meant to fix.
|
||||
for (ta::Event ev : tracker.Observe(finger, en::Mode::Authenticate)) {
|
||||
for (ta::Event ev : tracker.Observe(finger, settled, en::Mode::Authenticate)) {
|
||||
if (ev == ta::Event::FingerTouched) {
|
||||
inPress = true; pressMatched = false; pressRejected = false;
|
||||
pressFrames = 0; rescans = 0; pressFid = 0;
|
||||
|
|
@ -1521,9 +1529,11 @@ public:
|
|||
else std::this_thread::sleep_for(std::chrono::milliseconds(g_frameGapMs));
|
||||
}
|
||||
fingerPresent_.store(false);
|
||||
std::println(" verify loop: {} frames in {} ms ({} ms/frame incl. {} ms gap)",
|
||||
std::println(" verify loop: {} frames in {} ms ({} ms/frame incl. {} ms gap){}",
|
||||
out.frames, msSince(t0),
|
||||
out.frames ? msSince(t0) / out.frames : 0, g_frameGapMs);
|
||||
out.frames ? msSince(t0) / out.frames : 0, g_frameGapMs,
|
||||
skipped ? std::format(", {} unsettled frame(s) skipped", skipped) : "");
|
||||
out.skippedUnsettled = skipped;
|
||||
if (cancel) {
|
||||
SendCommand(app_, ta::Cmd::Cancel, {});
|
||||
out.cancelled = true;
|
||||
|
|
|
|||
Loading…
Reference in a new issue