Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0e94eca
feat(antispoofing): replace IR presence check with liveness classific…
Tunahanyrd Jun 7, 2026
bb2799a
feat(app,docs): show IR threshold in UI, update README and IR camera …
Tunahanyrd Jun 7, 2026
593cd84
refactor(antispoofing): implement secure fail-closed liveness thresho…
Tunahanyrd Jun 7, 2026
b44785a
docs(ir): clarify that anti-spoof model is RGB-trained, running on re…
Tunahanyrd Jun 7, 2026
0f3b9d5
refactor(antispoofing): capture IR frame synchronously before async t…
Tunahanyrd Jun 7, 2026
ba8c15e
refactor: sort face detections by confidence and clarify liveness sco…
Tunahanyrd Jun 7, 2026
b7f9b9c
refactor: match RGB and IR faces spatially, add early path validation…
Tunahanyrd Jun 7, 2026
0a1993c
refactor: revert global Detection operators and improve output tensor…
Tunahanyrd Jun 7, 2026
cb04a10
fix: sort face detections by confidence
Tunahanyrd Jun 7, 2026
a338e2f
refactor: log highest detection confidence on IR failure
Tunahanyrd Jun 11, 2026
e0dde87
Optimize IR camera warmup and add diagnostic logs/raw frame saving
Tunahanyrd Jun 11, 2026
13947b4
Implement 3-frame sequence aggregate liveness check with majority voting
Tunahanyrd Jun 11, 2026
eb56fe3
Optimize IR liveness authentication
Tunahanyrd Jun 11, 2026
fb602e8
docs: update IR liveness pipeline guide
Tunahanyrd Jun 11, 2026
cf95cd8
Refine IR liveness crop quality policy
Tunahanyrd Jun 11, 2026
f20a596
Calibrate IR liveness face scale threshold
Tunahanyrd Jun 11, 2026
6756e4c
fix: repair app lockfile
Tunahanyrd Jun 11, 2026
e992385
Tune IR face scale default
Tunahanyrd Jun 11, 2026
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Biopass was developed by [@phucvinh57](https://github.com/phucvinh57) and [@thai
| **Authentication methods** | Face + Fingerprint | Face only |
| **User Interface** | Modern GUI for management | Command-line interface only |
| **Configuration** | GUI | Manual |
| **Face Anti-spoofing** | IR camera + Embedded AI model | IR camera only |
| **Face Anti-spoofing** | IR liveness classification + Embedded AI model | IR camera only |

## Installation

Expand All @@ -62,6 +62,7 @@ Feel free to request new features or report bugs by opening an issue. For contri
Models used in this project:
- Face Recognition: **[EdgeFace](https://github.com/otroshi/edgeface)**
- Face Detection: **[YOLO-Face](https://github.com/akanametov/yolo-face)**
- Face Anti-Spoofing: **[mobilenetv3-antispoof](https://github.com/facenox/face-antispoof-onnx)** (used for both RGB and IR liveness classification)

## Star History

Expand Down
24 changes: 11 additions & 13 deletions app/bun.lock

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

6 changes: 3 additions & 3 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
"@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-router": "^1.168.18",
"@tanstack/react-router-devtools": "^1.166.13",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-opener": "^2.5.3",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-fs": "2.4.5",
"@tauri-apps/plugin-opener": "2.5.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.577.0",
Expand Down
16 changes: 16 additions & 0 deletions app/src-tauri/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ struct AntiSpoofingConfigRaw {
pub threshold: Option<f32>,
#[serde(default)]
pub ir_camera: Option<String>,
#[serde(default = "default_ir_warmup_delay_ms")]
pub ir_warmup_delay_ms: i32,
#[serde(default = "default_ir_min_face_area_ratio")]
pub ir_min_face_area_ratio: f32,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -160,6 +164,8 @@ pub struct AntiSpoofingConfig {
pub enable: bool,
pub model: AntiSpoofingModelConfig,
pub ir_camera: Option<String>,
pub ir_warmup_delay_ms: i32,
pub ir_min_face_area_ratio: f32,
}

impl AntiSpoofingConfig {
Expand Down Expand Up @@ -201,6 +207,8 @@ impl AntiSpoofingConfig {
enable: raw.enable,
model,
ir_camera: raw.ir_camera,
ir_warmup_delay_ms: raw.ir_warmup_delay_ms,
ir_min_face_area_ratio: raw.ir_min_face_area_ratio,
}
}
}
Expand Down Expand Up @@ -242,6 +250,12 @@ fn default_fingerprint_retries() -> u32 {
fn default_fingerprint_timeout() -> u32 {
5000
}
fn default_ir_warmup_delay_ms() -> i32 {
300
}
fn default_ir_min_face_area_ratio() -> f32 {
0.08
}

fn default_ignored_services() -> Vec<String> {
vec!["polkit-1".to_string(), "pkexec".to_string()]
Expand Down Expand Up @@ -282,6 +296,8 @@ fn get_default_config(app: &AppHandle) -> BiopassConfig {
threshold: 0.8,
},
ir_camera: None,
ir_warmup_delay_ms: default_ir_warmup_delay_ms(),
ir_min_face_area_ratio: default_ir_min_face_area_ratio(),
},
},
fingerprint: FingerprintMethodConfig {
Expand Down
2 changes: 1 addition & 1 deletion app/src/app/configuration/-components/face/FaceSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export function FaceSetting() {
</Select>
</div>

{config.anti_spoofing.enable && (
{(config.anti_spoofing.enable || !!config.anti_spoofing.ir_camera) && (
<div className="w-48">
<Threshold
label="Threshold"
Expand Down
2 changes: 2 additions & 0 deletions app/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface FaceMethodConfig {
threshold: number;
};
ir_camera: string | null;
ir_warmup_delay_ms: number;
ir_min_face_area_ratio: number;
};
}

Expand Down
21 changes: 20 additions & 1 deletion auth/core/auth_config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ BiopassConfig readConfig(const std::string& username) {
config.methods.face.anti_spoofing.ir_warmup_delay_ms =
anti_spoofing["ir_warmup_delay_ms"].as<int>();
}

if (anti_spoofing["ir_min_face_area_ratio"]) {
config.methods.face.anti_spoofing.ir_min_face_area_ratio =
anti_spoofing["ir_min_face_area_ratio"].as<float>();
}
}

// Backward compatibility with old schema:
Expand Down Expand Up @@ -258,6 +263,8 @@ bool migrateConfigSchema(const std::string& username, std::string* error) {
std::string model_path = "models/mobilenetv3_antispoof.onnx";
float threshold = 0.8f;
std::string ir_camera_path;
int ir_warmup_delay_ms = 300;
float ir_min_face_area_ratio = 0.08f;

if (anti) {
if (anti["enable"]) {
Expand All @@ -284,6 +291,14 @@ bool migrateConfigSchema(const std::string& username, std::string* error) {
if (anti["ir_camera"] && !anti["ir_camera"].IsNull()) {
ir_camera_path = anti["ir_camera"].as<std::string>();
}

if (anti["ir_warmup_delay_ms"]) {
ir_warmup_delay_ms = anti["ir_warmup_delay_ms"].as<int>();
}

if (anti["ir_min_face_area_ratio"]) {
ir_min_face_area_ratio = anti["ir_min_face_area_ratio"].as<float>();
}
}

if (ir_camera_path.empty() && face["ir_camera"]) {
Expand Down Expand Up @@ -311,9 +326,11 @@ bool migrateConfigSchema(const std::string& username, std::string* error) {
anti && static_cast<bool>(anti["model"]) && anti["model"].IsMap() &&
static_cast<bool>(anti["model"]["path"]) && static_cast<bool>(anti["model"]["threshold"]);
const bool has_new_ir_key = anti && static_cast<bool>(anti["ir_camera"]);
const bool has_new_ir_warmup_key = anti && static_cast<bool>(anti["ir_warmup_delay_ms"]);
const bool has_new_ir_area_key = anti && static_cast<bool>(anti["ir_min_face_area_ratio"]);
const bool needs_migration = has_legacy_face_ir || has_legacy_anti_threshold ||
has_legacy_anti_model_scalar || !has_new_model_map ||
!has_new_ir_key;
!has_new_ir_key || !has_new_ir_warmup_key || !has_new_ir_area_key;
if (!needs_migration)
return true;

Expand All @@ -328,6 +345,8 @@ bool migrateConfigSchema(const std::string& username, std::string* error) {
} else {
anti_new["ir_camera"] = ir_camera_path;
}
anti_new["ir_warmup_delay_ms"] = ir_warmup_delay_ms;
anti_new["ir_min_face_area_ratio"] = ir_min_face_area_ratio;

face["anti_spoofing"] = anti_new;
if (face["ir_camera"]) {
Expand Down
9 changes: 4 additions & 5 deletions auth/core/auth_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,12 @@ struct AntiSpoofingConfig {
AntiSpoofingModelConfig model;
// Linux device path, e.g. "/dev/video2". nullopt means disabled.
std::optional<std::string> ir_camera = std::nullopt;
// Extra delay (ms) inserted after the warmup-frame discard and before the
// actual IR capture. Gives IR LEDs and auto-exposure time to stabilise.
// Extra delay (ms) kept for backwards compatibility with existing configs.
// Configurable via anti_spoofing.ir_warmup_delay_ms in config.yaml.
// NOTE: The IR check verifies that a face-shaped bounding box exists in the
// IR frame (presence check). It is NOT a full liveness detector. A printed
// photo that the YOLO model can detect in IR will still pass.
int ir_warmup_delay_ms = 300;
// Minimum detected IR face bounding-box area as a fraction of the full frame.
// Smaller crops are treated as insufficient detail for reliable liveness.
float ir_min_face_area_ratio = 0.08f;
};

struct FaceMethodConfig {
Expand Down
Loading