# Face Detection Login System

This document covers the complete implementation of a biometric face detection login system built inside Oracle APEX 24.2. The system replaces or supplements the standard username/password login with a real-time webcam-based face recognition flow, powered entirely by face-api.js running in the browser — no external paid API required.

## 1.1 Technology Stack

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p><strong>Component</strong></p></td><td colspan="1" rowspan="1"><p><strong>Detail</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>UI Platform</strong></p></td><td colspan="1" rowspan="1"><p>Oracle APEX 24.2</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Database</strong></p></td><td colspan="1" rowspan="1"><p>Oracle Database 19c</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Face AI Library</strong></p></td><td colspan="1" rowspan="1"><p>face-api.js v0.22.2 (browser-side)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Face Models</strong></p></td><td colspan="1" rowspan="1"><p>TinyFaceDetector + FaceLandmark68 + FaceRecognition</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Authentication</strong></p></td><td colspan="1" rowspan="1"><p>Custom APEX Auth Scheme (PL/SQL function)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Data Storage</strong></p></td><td colspan="1" rowspan="1"><p>apex_face_users table (CLOB descriptor)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Login Page</strong></p></td><td colspan="1" rowspan="1"><p>Page 9999 (APEX default login page)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>App URL</strong></p></td><td colspan="1" rowspan="1"><p><a target="_self" rel="noopener noreferrer nofollow" class="cursor-pointer" href="https://oracleapex.com/ords/f?p=218621:LOGIN" style="pointer-events: none;">https://oracleapex.com/ords/f?p=218621:LOGIN</a></p></td></tr></tbody></table>

## 1.2 How It Works — Flow Summary

- User opens the APEX login page (Page 9999)
- Two buttons are visible: **Register Your Face** and **Login with Face Detection**
- Registration: user enters username → camera opens → face is auto-captured after 5 confirmed frames → 128-point descriptor saved to DB
- Login: camera opens → face scanned → descriptor compared against DB via Euclidean distance → if match ≤ 0.5 threshold → login submitted automatically
- APEX custom auth scheme validates the bypass password and confirms username exists in `apex_face_users`

## 2\. Database Setup

### 2.1 Face Users Table

**Run this SQL in SQL Workshop → SQL Commands:**

CREATE TABLE apex_face_users (\
id   NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,\
username  VARCHAR2(100) NOT NULL,\
face_descriptor  CLOB NOT NULL,\
created_date     DATE DEFAULT SYSDATE\
);

NOTE: face_descriptor stores a 128-element JSON float array like \[0.12, -0.34, 0.56, ...\]. It is NOT an image — do not use a file upload control to populate it.

### 2.2 Custom Authentication Function

Run this in SQL Workshop → SQL Commands:

CREATE OR REPLACE FUNCTION custom_face_auth ( p_username IN VARCHAR2, p_password IN VARCHAR2 ) RETURN BOOLEAN IS l_count NUMBER; BEGIN IF p_password = 'FACE_BYPASS_2024' THEN SELECT COUNT(\*) INTO l_count FROM apex_face_users WHERE UPPER(username) = UPPER(p_username); RETURN l_count > 0; END IF; RETURN FALSE; END;

### **3\. APEX Authentication Scheme**

Go to: Shared Components → Authentication Schemes → Create → From Scratch

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p><strong>Setting</strong></p></td><td colspan="1" rowspan="1"><p><strong>Value</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Name</strong></p></td><td colspan="1" rowspan="1"><p>Face Detection Auth</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Scheme Type</strong></p></td><td colspan="1" rowspan="1"><p>Custom</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Authentication Function</strong></p></td><td colspan="1" rowspan="1"><p>custom_face_auth</p></td></tr><tr><td colspan="1" rowspan="1"><p>After creating, make this scheme Current by clicking "Make Current Scheme" so APEX uses it for all logins.</p></td><td colspan="1" rowspan="1"><p>&nbsp;</p></td></tr></tbody></table>

## 4\. Model Files Setup

### 4.1 Download from GitHub

Download all 6 files from: <https://github.com/justadudewhohacks/face-api.js/tree/master/weights>

● tiny_face_detector_model-shard1\
● tiny_face_detector_model-weights_manifest.json\
● face_landmark_68_model-shard1\
● face_landmark_68_model-weights_manifest.json\
● face_recognition_model-shard1\
● face_recognition_model-shard2 ← critical, often missed\
● face_recognition_model-weights_manifest.json

### 4.2 Upload to APEX

Go to: Shared Components → Static Application Files → Upload

● Create a folder called models/\
● Upload all 7 files into the models/ folder\
● Reference path: #APP_FILES#models/

**IMPORTANT: face_recognition_model has TWO shards (shard1 and shard2). Missing shard2 causes a tensor shape error and models will not load**.

## 5\. APEX Page 9999 Setup

### 5.1 Existing Items (Pre-built by APEX)

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p><strong>Item</strong></p></td><td colspan="1" rowspan="1"><p><strong>Type</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Region</strong></p></td><td colspan="1" rowspan="1"><p>Face Detection (Content Body)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>P9999_USERNAME</strong></p></td><td colspan="1" rowspan="1"><p>Text Field</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>P9999_PASSWORD</strong></p></td><td colspan="1" rowspan="1"><p>Text Field (Password)</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>P9999_REMEMBER</strong></p></td><td colspan="1" rowspan="1"><p>Checkbox</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>LOGIN button</strong></p></td><td colspan="1" rowspan="1"><p>Submit → request = LOGIN</p></td></tr></tbody></table>

## 5.2 New Buttons to Create

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p><strong>Button Name</strong></p></td><td colspan="1" rowspan="1"><p><strong>Configuration</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Login with Face Detection</strong></p></td><td colspan="1" rowspan="1"><p>Action: Defined by Dynamic Action → DA calls openFaceDetection()</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Register Your Face</strong></p></td><td colspan="1" rowspan="1"><p>Action: Defined by Dynamic Action → DA calls openRegisterModal()</p></td></tr></tbody></table>

## 5.3 Dynamic Actions

For each button create a Dynamic Action:

●        Event: Click\
●        Selection Type: Button → select the button\
●        TRUE Action: Execute JavaScript Code\
○        Login button → Code: openFaceDetection()\
○        Register button → Code: openRegisterModal()\
Fire on Page Load: NO

## 5.4 HTML Regions to Create

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p><strong>Region Name</strong></p></td><td colspan="1" rowspan="1"><p><strong>Configuration</strong></p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Registration Modal</strong></p></td><td colspan="1" rowspan="1"><p>Type: Static Content | Position: Content Body | Paste registration HTML</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Verification Overlay</strong></p></td><td colspan="1" rowspan="1"><p>Type: Static Content | Position: Content Body | Paste verification HTML</p></td></tr></tbody></table>

## 6\. AJAX Callback Processes

Go to: Page 9999 → Processing → right-click → Create Process → AJAX Callback

### 6.1 REGISTER_FACE - Saves a new face descriptor or updates existing one:

DECLARE l_username VARCHAR2(100) := apex_application.g_x01; l_descriptor CLOB := apex_application.g_x02; l_count NUMBER; BEGIN SELECT COUNT(\*) INTO l_count FROM apex_face_users WHERE username = l_username; IF l_count > 0 THEN UPDATE apex_face_users SET face_descriptor = l_descriptor, created_date = SYSDATE WHERE username = l_username; ELSE INSERT INTO apex_face_users (username, face_descriptor) VALUES (l_username, l_descriptor); END IF; COMMIT; htp.p('{"status":"success","username":"' || l_username || '"}' ); EXCEPTION WHEN OTHERS THEN htp.p('{"status":"error","message":"' || SQLERRM || '"}' ); END;

### 6.2 VERIFY_FACE Returns all stored face descriptors for JS-side Euclidean comparison:

DECLARE l_json CLOB := '\['; l_first BOOLEAN := TRUE; BEGIN FOR r IN (SELECT username, face_descriptor FROM apex_face_users) LOOP IF NOT l_first THEN l_json := l_json || ','; END IF; l_json := l_json || '{"username":"' || r.username || '","descriptor":' || r.face_descriptor || '}'; l_first := FALSE; END LOOP; l_json := l_json || '\]'; apex_util.set_session_state('P9999_STATUS', l_json); htp.p(l_json); END;

### 6.3 DO_FACE_LOGIN Sets session state before page submit:

BEGIN apex_util.set_session_state('P9999_USERNAME', apex_application.g_x01); htp.p('{"status":"ok"}'); END;

## 7\. JavaScript — Execute when Page Loads

### Paste the entire block below into: Page 9999 → JavaScript → Execute when Page Loads

### 7.1 Model Loading

const MODEL_URL = '<https://oracleapex.com/ords/r/unionsys/face-detection/files/static/v1/models>'; let cameraStream = null, detectionInterval = null, modelsLoaded = false; const faceScript = document.createElement('script'); faceScript.src = '<https://cdn.jsdelivr.net/npm/face-api.js@0.22.2/dist/face-api.min.js>'; faceScript.onload = async function () { await faceapi.nets.tinyFaceDetector.loadFromUri(MODEL_URL); await faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL); await faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL); modelsLoaded = true; console.log('Face models loaded.'); }; document.head.appendChild(faceScript);

### 7.2 Key Fix — Landmark Accuracy The most important fix for accurate face landmark rendering (eyes, lips, nose in correct position): // After video loads, match canvas dimensions to display size

var displaySize = { width: video.offsetWidth, height: video.offsetHeight }; faceapi.matchDimensions(canvas, displaySize);

### // Before drawing detections, results to display size //

var resized = faceapi.resizeResults(det, displaySize); faceapi.draw.drawDetections(canvas, resized); faceapi.draw.drawFaceLandmarks(canvas, resized);

### 7.3 Login Flow On successful face match, the JS sets the APEX items and submits the login:

apex.item('P9999_USERNAME').setValue(username); apex.item('P9999_PASSWORD').setValue('FACE_BYPASS_2024'); apex.page.submit({ request: 'LOGIN' }); The custom_face_auth function then checks apex_face_users and returns TRUE if username exists.

## 8\. Bugs Fixed Along the Way

| **Bug** | **Fix** |
| --- | --- |
| Page submitting on button click | HTML buttons default to `type="submit"` — fixed by adding `type="button"` to all buttons in HTML regions |
| `openFaceDetection` is not defined | Functions declared inside `onload` callback were not global — fixed by changing all to `window.functionName = function()` |
| `face_recognition_model` 404 error | `shard2` file was missing — fixed by downloading and uploading `face_recognition_model-shard2` |
| Tensor shape mismatch error | Caused by the missing `shard2` — resolved when `shard2` was uploaded |
| Landmarks in wrong position | Canvas native size did not match display size — fixed using `faceapi.matchDimensions` + `faceapi.resizeResults` |
| Face captured but not saving to DB | Button without `type="button"` was submitting the page before AJAX completed |
| Login not working after face match | `DO_FACE_LOGIN` alone does not create session — fixed by submitting the `LOGIN` request with `FACE_BYPASS_2024` |

## 9\. Complete End-to-End Flow

### 9.1 Registration Flow

●        User clicks Register Your Face button\
●        openRegisterModal() fires via Dynamic Action\
●        Modal opens — Step 1 shown: username input + Continue button\
●        User enters username and clicks Continue\
●        startRegisterCamera() fires — webcam starts\
●        Step 2 shown: ring with live video feed\
●        faceapi detects face every 200ms using TinyFaceDetector\
●        faceapi.matchDimensions ensures landmarks render accurately\
●        5 confirmed frames fill the progress dots\
●        AUTO CAPTURE fires — no button click needed\
●        Ring turns green with pulsing glow\
●        REGISTER_FACE AJAX process called with username (x01) + descriptor JSON (x02)\
●        PL/SQL inserts or updates apex_face_users table\
●        Success message shown, modal closes after 2 seconds

9.2 Login Flow

● User clicks Login with Face Detection button\
● openFaceDetection() fires via Dynamic Action\
● Verification overlay opens with violet/indigo pulsing ring\
● Webcam starts, faceapi runs every 200ms\
● 5 confirmed frames fill progress dots (amber color)\
● VERIFY_FACE AJAX called — PL/SQL returns all stored descriptors as JSON\
● JS loops through stored descriptors and calculates Euclidean distance\
● Best match checked against threshold 0.5\
● Match found → ring turns green → Welcome message shown\
● After 1.8s: P9999_USERNAME = matched username, P9999_PASSWORD = FACE_BYPASS_2024, page submitted\
● custom_face_auth function runs → checks apex_face_users → returns TRUE → user logged in\
● APEX redirects to home page\
● No match → ring turns red → Access Denied → overlay closes after 3 seconds

## 10\. Troubleshooting Quick Reference

| **Symptom** | **Solution** |
| --- | --- |
| Models not loading | Check F12 console for 404 — verify model URL matches static file path in Shared Components |
| Button does nothing | Check Dynamic Action → TRUE Action is Execute JavaScript Code with correct function name |
| Page jumps back on camera open | Button inside HTML region is missing `type="button"` |
| Face not detected | Improve lighting, move closer, use Chrome browser |
| Always says Access Denied | No data in `apex_face_users` — register a face first |
| Login fails after green ring | Verify `custom_face_auth` function exists in DB and Authentication Scheme is set to Current |
| Landmarks shifted/offset | `faceapi.matchDimensions` or `faceapi.resizeResults` missing in JS |
| `shard2` 404 error | Upload `face_recognition_model-shard2` to **Static Application Files → models/** |

## 11\. Security Notes

The FACE_BYPASS_2024 password is a shared secret used to bypass normal password auth. For production use, consider rotating this value and storing it in APEX Application Settings rather than hardcoding in JS.

● Face descriptors are stored as plain JSON in CLOB — consider encrypting at rest for production\
● Euclidean threshold of 0.5 balances accuracy vs security — lower = stricter\
● For multi-user environments, register multiple face angles per user for better accuracy\
● HTTPS is required for webcam access in all modern browsers\
● This implementation is suitable for internal tools and demos — for banking/high security apps, use a dedicated biometric SDK

**Thank You for your time.**

If you need any help or have any questions, feel free to leave a comment or contact me via email.

**Email:** [arfaats999@gmail.com](mailto:arfaats999@gmail.com)
