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
|
|
@ -69,9 +69,33 @@ export namespace fingerprintd::engine {
|
|||
}
|
||||
}
|
||||
|
||||
// A SECOND, higher bar: contact good enough to spend the press's one
|
||||
// terminal verdict on. Detection at 1.5x answers "is a finger there";
|
||||
// this answers "is it all the way down and still".
|
||||
//
|
||||
// Measured over every frame this project has a verdict for (2026-09-05,
|
||||
// n=379), split at 2.5x the 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 partial frame is not a hard verdict waiting to happen, it is a
|
||||
// WASTED one: with max_authentication_rescan_times at 0 every frame is
|
||||
// terminal, so a partial frame's rejection ends the press. 62 partial
|
||||
// frames produced exactly one match. Skipping them costs essentially
|
||||
// nothing and saves 61 killed presses.
|
||||
static constexpr std::int32_t SettledNum = 5;
|
||||
static constexpr std::int32_t SettledDen = 2;
|
||||
|
||||
bool Ready() const { return seen_ >= want_ && floor_ > 0; }
|
||||
std::int32_t Floor() const { return floor_; }
|
||||
std::int32_t Threshold() const { return floor_ * MultiplierNum / MultiplierDen; }
|
||||
std::int32_t SettledThreshold() const { return floor_ * SettledNum / SettledDen; }
|
||||
bool IsSettled(std::int32_t metric) const {
|
||||
return Ready() && metric >= SettledThreshold();
|
||||
}
|
||||
|
||||
// Nothing is a finger until the floor is known. An uncalibrated
|
||||
// Baseline reports false for everything rather than inventing a
|
||||
|
|
@ -115,8 +139,24 @@ export namespace fingerprintd::engine {
|
|||
class TouchTracker {
|
||||
public:
|
||||
// Returns the events to report for this frame, in order.
|
||||
std::vector<Event> Observe(bool finger, Mode mode) {
|
||||
//
|
||||
// TWO thresholds, deliberately: a press STARTS on settled contact and
|
||||
// ENDS on the finger leaving. A Schmitt trigger, and the reason is
|
||||
// measured -- a frame taken while the finger is still arriving matches
|
||||
// 0 times in 53, and at a rescan budget of 0 that rejection is terminal
|
||||
// and ends the press. Starting the press on the settled frame instead
|
||||
// spends the verdict on an image that can actually carry it. A press
|
||||
// that never settles produces no event at all, which is an honest
|
||||
// "try again" rather than a false rejection.
|
||||
//
|
||||
// `settled` defaults to `present` so enrolment, which has its own
|
||||
// sample-quality gate inside the trustlet, is unchanged.
|
||||
std::vector<Event> Observe(bool present, Mode mode) {
|
||||
return Observe(present, present, mode);
|
||||
}
|
||||
std::vector<Event> Observe(bool present, bool settled, Mode mode) {
|
||||
std::vector<Event> out;
|
||||
bool finger = prev_ ? present : settled; // enter high, leave low
|
||||
bool rising = finger && !prev_;
|
||||
bool falling = !finger && prev_;
|
||||
if (rising)
|
||||
|
|
|
|||
Loading…
Reference in a new issue