diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/__init__.py b/__init__.py old mode 100644 new mode 100755 diff --git a/__manifest__.py b/__manifest__.py old mode 100644 new mode 100755 diff --git a/ai_face_recognition_server/README.md b/ai_face_recognition_server/README.md new file mode 100644 index 0000000..31e0956 --- /dev/null +++ b/ai_face_recognition_server/README.md @@ -0,0 +1,61 @@ +# AI Face Recognition Server + +A Flask-based standalone server for face recognition, designed for integration with Odoo Point of Sale (POS). + +## Features +- **Real-time Recognition**: Processes images and returns matches with metadata (ID, Name, Probability). +- **Face Training**: API endpoint to register new faces from base64 images. +- **Production Ready**: Supports Gunicorn for stability and concurrency. +- **Optimized for POS**: Default 60% match threshold handles varied lighting conditions well. + +## Prerequisites +- Python 3.10+ +- System libraries for `dlib` (cmake, g++, etc.) +- Virtual environment (recommended) + +## Installation + +1. **Activate the Virtual Environment**: + ```bash + source .venv/bin/activate + ``` + +2. **Install Dependencies**: + ```bash + pip install -r requirements.txt + ``` + +## Running the Server + +### Production (Recommended) +Run using Gunicorn for better performance and stability: +```bash +.venv/bin/gunicorn -w 1 -b 0.0.0.0:5000 app:app --timeout 120 +``` + +### Development +```bash +python app.py +``` + +## API Endpoints + +### 1. Recognize Face +- **Endpoint**: `POST /recognize` +- **Body**: `{ "image": "base64_string_without_header" }` +- **Response**: List of matches with `id`, `name`, and `probability`. + +### 2. Train Face +- **Endpoint**: `POST /train` +- **Body**: `{ "partner_id": 123, "name": "John Doe", "images": ["base64_1", "base64_2"] }` +- **Description**: Saves images to the `faces/` directory and updates the in-memory model. + +## Matching Logic +- **Tolerance**: `0.4` (Distance) +- **Probability**: `1.0 - distance` (Matches >= 60% are returned) +- **Diagnostics**: Server logs the exact distance and probability to the terminal for every request. + +## Directory Structure +- `app.py`: Main Flask application. +- `faces/`: Directory where trained face images are stored. +- `requirements.txt`: Python dependencies. diff --git a/ai_face_recognition_server/app.py b/ai_face_recognition_server/app.py old mode 100644 new mode 100755 index 4a8c3c6..5acc60e --- a/ai_face_recognition_server/app.py +++ b/ai_face_recognition_server/app.py @@ -82,8 +82,8 @@ def recognize_face(): matches_list = [] for face_encoding in face_encodings: - # See if the face is a match for the known face(s) - matches = face_recognition.compare_faces(known_face_encodings, face_encoding) + # Use threshold of 0.4 for slightly less strict matching (around 60% match) + matches = face_recognition.compare_faces(known_face_encodings, face_encoding, tolerance=0.4) name = "Unknown" p_id = 0 probability = 0.0 @@ -96,15 +96,18 @@ def recognize_face(): name = known_face_names[best_match_index] p_id = known_face_ids[best_match_index] # Simple probability estimation based on distance (lower distance = higher prob) - # Distance 0.0 -> 100%, Distance 0.6 -> ~0% (threshold) + # Distance 0.0 -> 100%, Distance 0.3 -> 70% distance = face_distances[best_match_index] probability = max(0, 1.0 - distance) + + print(f"Best match for current face: {name} (ID: {p_id}) with distance {distance:.3f} (Prob: {probability:.3f})") - matches_list.append({ - 'id': p_id, - 'name': name, - 'probability': probability - }) + if probability >= 0.6: + matches_list.append({ + 'id': p_id, + 'name': name, + 'probability': probability + }) # Sort by probability matches_list.sort(key=lambda x: x['probability'], reverse=True) @@ -165,6 +168,7 @@ def train_face(): return jsonify({'message': f'Processed {processed_count} images for {name}'}) +load_known_faces() + if __name__ == '__main__': - load_known_faces() app.run(host='0.0.0.0', port=5000) diff --git a/ai_face_recognition_server/faces/19_Suherdy_Yacob_58_0.jpg b/ai_face_recognition_server/faces/19_Suherdy_Yacob_58_0.jpg old mode 100644 new mode 100755 diff --git a/ai_face_recognition_server/faces/19_Suherdy_Yacob_58_1.jpg b/ai_face_recognition_server/faces/19_Suherdy_Yacob_58_1.jpg old mode 100644 new mode 100755 diff --git a/ai_face_recognition_server/faces/19_Suherdy_Yacob_58_2.jpg b/ai_face_recognition_server/faces/19_Suherdy_Yacob_58_2.jpg old mode 100644 new mode 100755 diff --git a/ai_face_recognition_server/faces/311_Trisna_Adi_saputra_0.jpg b/ai_face_recognition_server/faces/311_Trisna_Adi_saputra_0.jpg new file mode 100755 index 0000000..333ce60 Binary files /dev/null and b/ai_face_recognition_server/faces/311_Trisna_Adi_saputra_0.jpg differ diff --git a/ai_face_recognition_server/faces/311_Trisna_Adi_saputra_1.jpg b/ai_face_recognition_server/faces/311_Trisna_Adi_saputra_1.jpg new file mode 100755 index 0000000..635bf48 Binary files /dev/null and b/ai_face_recognition_server/faces/311_Trisna_Adi_saputra_1.jpg differ diff --git a/ai_face_recognition_server/faces/7769_Martha_A_58_0.jpg b/ai_face_recognition_server/faces/7769_Martha_A_58_0.jpg new file mode 100755 index 0000000..4f95ac8 Binary files /dev/null and b/ai_face_recognition_server/faces/7769_Martha_A_58_0.jpg differ diff --git a/ai_face_recognition_server/faces/7769_Martha_A_58_1.jpg b/ai_face_recognition_server/faces/7769_Martha_A_58_1.jpg new file mode 100755 index 0000000..c7c93d4 Binary files /dev/null and b/ai_face_recognition_server/faces/7769_Martha_A_58_1.jpg differ diff --git a/ai_face_recognition_server/requirements.txt b/ai_face_recognition_server/requirements.txt old mode 100644 new mode 100755 index 056574c..9a669ee --- a/ai_face_recognition_server/requirements.txt +++ b/ai_face_recognition_server/requirements.txt @@ -3,3 +3,5 @@ flask-cors face_recognition numpy opencv-python +setuptools +gunicorn diff --git a/models/__init__.py b/models/__init__.py old mode 100644 new mode 100755 diff --git a/models/pos_config.py b/models/pos_config.py old mode 100644 new mode 100755 diff --git a/models/res_config_settings.py b/models/res_config_settings.py old mode 100644 new mode 100755 diff --git a/models/res_partner.py b/models/res_partner.py old mode 100644 new mode 100755 diff --git a/static/src/css/face_recognition.css b/static/src/css/face_recognition.css old mode 100644 new mode 100755 diff --git a/static/src/js/face_recognition_sidebar.js b/static/src/js/face_recognition_sidebar.js old mode 100644 new mode 100755 index 46061aa..30b5442 --- a/static/src/js/face_recognition_sidebar.js +++ b/static/src/js/face_recognition_sidebar.js @@ -59,17 +59,17 @@ export class FaceRecognitionSidebar extends Component { startRealTimeRecognition() { console.log("startRealTimeRecognition called"); console.log("Server URL:", this.pos.config.pos_face_rec_server_url); - + if (!this.pos.config.pos_face_rec_server_url) { console.error("Face Recognition Server URL is not configured. Please configure it in POS Settings."); return; } - - // Capture frame every 5 seconds + + // Capture frame every 3 seconds this.recognitionInterval = setInterval(() => { console.log("Recognition interval tick"); this.captureAndSendFrame(); - }, 5000); + }, 3000); } async captureAndSendFrame() { @@ -78,7 +78,7 @@ export class FaceRecognitionSidebar extends Component { console.error("Video element not found"); return; } - + if (!this.pos.config.pos_face_rec_server_url) { console.error("Face Recognition Server URL is not configured"); return; @@ -103,14 +103,19 @@ export class FaceRecognitionSidebar extends Component { if (response.ok) { const result = await response.json(); if (result.matches && result.matches.length > 0) { + // Filter matches (ensure only >= 60% probability) + const filteredMatches = result.matches.filter(match => match.probability >= 0.6); + // Enrich matches with partner phone numbers - this.state.matches = result.matches.map(match => { + this.state.matches = filteredMatches.map(match => { const partner = this.pos.db.get_partner_by_id(match.id); return { ...match, phone: partner ? partner.phone || partner.mobile : null }; }); + } else { + this.state.matches = []; } } } catch (err) { @@ -119,13 +124,18 @@ export class FaceRecognitionSidebar extends Component { } async selectCustomer(match) { + console.log("selectCustomer called with match:", match); const partner = this.pos.db.get_partner_by_id(match.id); + console.log("Partner found in DB:", partner); if (partner) { this.state.selectedCustomer = partner; this.pos.get_order().set_partner(partner); + console.log("Partner set to current order:", this.pos.get_order().get_partner()); // Fetch details await this.fetchCustomerDetails(partner.id); + } else { + console.error("Partner not found for ID:", match.id); } } @@ -136,7 +146,7 @@ export class FaceRecognitionSidebar extends Component { ['name', 'date_order', 'amount_total', 'state', 'lines'], { limit: 2, order: "date_order desc" } ); - + // Fetch order lines for each order for (let order of orders) { if (order.lines && order.lines.length > 0) { @@ -154,7 +164,7 @@ export class FaceRecognitionSidebar extends Component { order.lines = []; } } - + this.state.lastOrders = orders; // Fetch top 3 products @@ -163,7 +173,7 @@ export class FaceRecognitionSidebar extends Component { console.log("Top products received:", topProducts); console.log("Type:", typeof topProducts); console.log("First product:", topProducts && topProducts[0]); - + // Ensure it's an array if (Array.isArray(topProducts)) { this.state.topProducts = topProducts; diff --git a/static/src/js/models.js b/static/src/js/models.js old mode 100644 new mode 100755 diff --git a/static/src/js/partner_details_edit.js b/static/src/js/partner_details_edit.js old mode 100644 new mode 100755 diff --git a/static/src/xml/face_recognition_screens.xml b/static/src/xml/face_recognition_screens.xml old mode 100644 new mode 100755 index f511724..9129dd3 --- a/static/src/xml/face_recognition_screens.xml +++ b/static/src/xml/face_recognition_screens.xml @@ -22,7 +22,7 @@

Matches: