Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ private static boolean processBufferInternal(
@Nullable Image image) {
final long inferenceStartedNs = System.nanoTime();

final FaceCandidate faceCandidate =
findFaceCandidateFast(image, yBuffer, width, height, stride);
final FaceCandidate faceCandidate = image != null && image.getPlanes().length >= 3
? findFaceCandidate(image, yBuffer, width, height, stride)
: findFaceCandidateFast(image, yBuffer, width, height, stride);
if (!faceCandidate.faceLike) {
final float inferenceMs =
(System.nanoTime() - inferenceStartedNs) / 1_000_000.0f;
Expand Down Expand Up @@ -180,9 +181,9 @@ private static FaceCandidate findFaceCandidateFast(
if (!frameHasEnoughTexture(yBuffer, width, height, stride)) {
return FaceCandidate.none();
}
// Try single-orientation Android detector with front camera default (270°)
// Try the legacy detector across supported frame orientations.
final FaceCandidate detectorCandidate =
detectFaceCandidateAtOrientation(yBuffer, width, height, stride, 270);
detectFaceCandidateWithAndroidDetector(yBuffer, width, height, stride);
if (detectorCandidate.faceLike) {
return detectorCandidate;
}
Expand Down Expand Up @@ -383,8 +384,10 @@ private static FaceMeshOutput runFaceMesh(
}
final boolean confidenceAllowsFace =
!hasPresenceScore || presenceScore >= FACE_PRESENCE_HARD_REJECT_THRESHOLD;
final boolean geometryAllowsFace = landmarksLookLikeFace(landmarks, width, height);
final boolean facePresent =
confidenceAllowsFace && landmarksLookLikeFace(landmarks, width, height);
geometryAllowsFace &&
(confidenceAllowsFace || candidate.confidence >= STRONG_FACE_CANDIDATE_CONFIDENCE);
return new FaceMeshOutput(landmarks, facePresent, presenceScore);
}

Expand Down Expand Up @@ -1036,7 +1039,7 @@ private static boolean landmarksLookLikeFace(float[] values, int width, int heig
eyeRatio(values, 362, 385, 387, 263, 373, 380));
final float mar = mouthRatio(values);
return boxToEye >= 1.35f && boxToEye <= 3.60f &&
ear >= 0.03f && ear <= 0.60f &&
ear >= 0.03f && ear <= 0.85f &&
mar >= 0.01f && mar <= 1.20f;
}

Expand Down
42 changes: 35 additions & 7 deletions cpp/landmarks/LivenessFSM.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "LivenessFSM.h"
#include <algorithm>
#include <cmath>
#include <utility>

Expand Down Expand Up @@ -108,6 +109,7 @@ void LivenessFSM::StartChallenge(LivenessChallenge challenge,
smileStartedAt_ = now;
blinkWasClosed_ = false;
blinkBaselineCaptured_ = false;
blinkMinEarDuringClosure_ = 0.0f;
smileBaselineCaptured_ = false;
challengeSatisfied_ = false;
turnBaselineCaptured_ = false;
Expand All @@ -126,6 +128,7 @@ void LivenessFSM::Reset(Clock::time_point now) {
smileStartedAt_ = now;
baselineYaw_ = 0.0f;
baselineEar_ = 0.0f;
blinkMinEarDuringClosure_ = 0.0f;
baselineMar_ = 0.0f;
blinkWasClosed_ = false;
blinkBaselineCaptured_ = false;
Expand Down Expand Up @@ -211,27 +214,52 @@ void LivenessFSM::EvaluateBlink(const LivenessInput& input) {
return;
}

const float dynamicClosedEar = std::max(
thresholds_.blinkClosedEar,
baselineEar_ * 0.78f);
const float dynamicOpenEar =
std::max(dynamicClosedEar + 0.015f, baselineEar_ * 0.88f);
const bool scaledEar =
baselineEar_ > thresholds_.blinkOpenEar * thresholds_.blinkScaledEarMultiplier;
const bool relativeEar = baselineEar_ < thresholds_.blinkClosedEar;
const float dynamicClosedEar = scaledEar
? baselineEar_ - std::max(thresholds_.blinkScaledMinCloseDrop,
baselineEar_ * thresholds_.blinkScaledCloseDropRatio)
: relativeEar
? baselineEar_ - std::max(thresholds_.blinkRelativeMinCloseDrop,
baselineEar_ * thresholds_.blinkRelativeCloseDropRatio)
: std::max(thresholds_.blinkClosedEar, baselineEar_ * 0.78f);
const float dynamicOpenEar = scaledEar
? baselineEar_ - std::max(thresholds_.blinkScaledMinCloseDrop * 0.5f,
baselineEar_ * thresholds_.blinkScaledOpenDropRatio)
: relativeEar
? dynamicClosedEar + std::max(thresholds_.blinkRelativeMinRecovery,
baselineEar_ * thresholds_.blinkRelativeRecoveryRatio)
: std::max(
dynamicClosedEar + thresholds_.blinkRecoveryEar,
std::min(thresholds_.blinkOpenEar, baselineEar_ * 0.88f));
const float recoveryDelta = scaledEar
? std::max(thresholds_.blinkScaledMinCloseDrop,
baselineEar_ * thresholds_.blinkScaledCloseDropRatio)
: relativeEar
? std::max(thresholds_.blinkRelativeMinRecovery,
baselineEar_ * thresholds_.blinkRelativeRecoveryRatio)
: thresholds_.blinkRecoveryEar;

if (!blinkWasClosed_ && ear <= dynamicClosedEar) {
blinkWasClosed_ = true;
blinkClosedAt_ = input.timestamp;
blinkMinEarDuringClosure_ = ear;
reason_ = "blink closed";
return;
}

if (blinkWasClosed_) {
blinkMinEarDuringClosure_ = std::min(blinkMinEarDuringClosure_, ear);
if (input.timestamp - blinkClosedAt_ > thresholds_.blinkWindow) {
blinkWasClosed_ = false;
blinkMinEarDuringClosure_ = 0.0f;
reason_ = "blink window expired";
return;
}

if (ear >= dynamicOpenEar) {
if (ear >= dynamicOpenEar ||
ear - blinkMinEarDuringClosure_ >= recoveryDelta) {
Pass("blink challenge passed");
}
}
Expand Down Expand Up @@ -280,7 +308,7 @@ void LivenessFSM::EvaluateTurn(const LivenessInput& input, bool left) {
const float delta = input.metrics.yaw - baselineYaw_;
const bool passed = left ? delta <= -thresholds_.yawDeltaDegrees
: delta >= thresholds_.yawDeltaDegrees;
if (passed && input.timestamp - challengeStartedAt_ <= thresholds_.turnWindow) {
if (passed) {
Pass(left ? "turn left challenge passed" : "turn right challenge passed");
}
}
Expand Down
14 changes: 12 additions & 2 deletions cpp/landmarks/LivenessFSM.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,23 @@ enum class LivenessChallenge : uint8_t {
struct LivenessThresholds {
float blinkClosedEar{0.21f};
float blinkOpenEar{0.28f};
float blinkRecoveryEar{0.04f};
float blinkScaledEarMultiplier{1.5f};
float blinkScaledCloseDropRatio{0.01f};
float blinkScaledOpenDropRatio{0.004f};
float blinkScaledMinCloseDrop{0.006f};
float blinkRelativeCloseDropRatio{0.06f};
float blinkRelativeMinCloseDrop{0.008f};
float blinkRelativeRecoveryRatio{0.035f};
float blinkRelativeMinRecovery{0.006f};
std::chrono::milliseconds blinkWindow{800};
float smileMar{0.45f};
std::chrono::milliseconds smileSustain{600};
float yawDeltaDegrees{8.0f};
float yawDeltaDegrees{6.0f};
std::chrono::milliseconds turnWindow{2000};
std::chrono::milliseconds challengeTimeout{4000};
std::chrono::milliseconds failResetDelay{2000};
std::chrono::milliseconds faceDropoutTolerance{300};
std::chrono::milliseconds faceDropoutTolerance{1000};
};

struct LivenessInput {
Expand Down Expand Up @@ -99,6 +108,7 @@ class LivenessFSM {
std::chrono::steady_clock::time_point smileStartedAt_;
float baselineYaw_{0.0f};
float baselineEar_{0.0f};
float blinkMinEarDuringClosure_{0.0f};
float baselineMar_{0.0f};
bool blinkWasClosed_{false};
bool blinkBaselineCaptured_{false};
Expand Down
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ type NativeBridgeModule = {
type NativeChallenge = 'NONE' | 'BLINK' | 'SMILE' | 'TURN_LEFT' | 'TURN_RIGHT';

const MODEL_PATH = '/sdcard/Download/mobilefacenet.tflite';
const LIVENESS_STATE_NAMES = [
'IDLE',
'DETECTED',
'CHALLENGE_ACTIVE',
'LIVENESS_PASS',
'LIVENESS_FAIL',
] as const;
const LIVENESS_CHALLENGE_NAMES = [
'NONE',
'BLINK',
'SMILE',
'TURN_LEFT',
'TURN_RIGHT',
] as const;

const styles = StyleSheet.create({
root: {flex: 1, backgroundColor: '#0f172a'},
Expand Down Expand Up @@ -184,6 +198,10 @@ function formatResult(result: OfflineFaceAuthResult): string {
const preview = isUsableEmbedding(result)
? embeddingArray.slice(0, 16).map((value) => value.toFixed(6))
: [];
const livenessStateName =
LIVENESS_STATE_NAMES[result.livenessState ?? 0] ?? 'UNKNOWN';
const livenessChallengeName =
LIVENESS_CHALLENGE_NAMES[result.livenessChallenge ?? 0] ?? 'UNKNOWN';
return JSON.stringify(
{
accepted: result.accepted,
Expand All @@ -197,7 +215,9 @@ function formatResult(result: OfflineFaceAuthResult): string {
faceMeshThreadCount: result.faceMeshThreadCount,
mobileFaceNetThreadCount: result.mobileFaceNetThreadCount,
livenessState: result.livenessState,
livenessStateName,
livenessChallenge: result.livenessChallenge,
livenessChallengeName,
faceDetected: result.faceDetected,
ear: result.ear,
mar: result.mar,
Expand Down
Loading