feat: Improve face recognition accuracy and speed by adjusting server tolerance, filtering matches, and updating real-time recognition interval.
0
.gitignore
vendored
Normal file → Executable file
0
__init__.py
Normal file → Executable file
0
__manifest__.py
Normal file → Executable file
61
ai_face_recognition_server/README.md
Normal file
@ -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.
|
||||||
12
ai_face_recognition_server/app.py
Normal file → Executable file
@ -82,8 +82,8 @@ def recognize_face():
|
|||||||
matches_list = []
|
matches_list = []
|
||||||
|
|
||||||
for face_encoding in face_encodings:
|
for face_encoding in face_encodings:
|
||||||
# See if the face is a match for the known face(s)
|
# Use threshold of 0.4 for slightly less strict matching (around 60% match)
|
||||||
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
|
matches = face_recognition.compare_faces(known_face_encodings, face_encoding, tolerance=0.4)
|
||||||
name = "Unknown"
|
name = "Unknown"
|
||||||
p_id = 0
|
p_id = 0
|
||||||
probability = 0.0
|
probability = 0.0
|
||||||
@ -96,10 +96,13 @@ def recognize_face():
|
|||||||
name = known_face_names[best_match_index]
|
name = known_face_names[best_match_index]
|
||||||
p_id = known_face_ids[best_match_index]
|
p_id = known_face_ids[best_match_index]
|
||||||
# Simple probability estimation based on distance (lower distance = higher prob)
|
# 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]
|
distance = face_distances[best_match_index]
|
||||||
probability = max(0, 1.0 - distance)
|
probability = max(0, 1.0 - distance)
|
||||||
|
|
||||||
|
print(f"Best match for current face: {name} (ID: {p_id}) with distance {distance:.3f} (Prob: {probability:.3f})")
|
||||||
|
|
||||||
|
if probability >= 0.6:
|
||||||
matches_list.append({
|
matches_list.append({
|
||||||
'id': p_id,
|
'id': p_id,
|
||||||
'name': name,
|
'name': name,
|
||||||
@ -165,6 +168,7 @@ def train_face():
|
|||||||
|
|
||||||
return jsonify({'message': f'Processed {processed_count} images for {name}'})
|
return jsonify({'message': f'Processed {processed_count} images for {name}'})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
load_known_faces()
|
load_known_faces()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000)
|
app.run(host='0.0.0.0', port=5000)
|
||||||
|
|||||||
0
ai_face_recognition_server/faces/19_Suherdy_Yacob_58_0.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
0
ai_face_recognition_server/faces/19_Suherdy_Yacob_58_1.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
0
ai_face_recognition_server/faces/19_Suherdy_Yacob_58_2.jpg
Normal file → Executable file
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
BIN
ai_face_recognition_server/faces/311_Trisna_Adi_saputra_0.jpg
Executable file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
ai_face_recognition_server/faces/311_Trisna_Adi_saputra_1.jpg
Executable file
|
After Width: | Height: | Size: 58 KiB |
BIN
ai_face_recognition_server/faces/7769_Martha_A_58_0.jpg
Executable file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
ai_face_recognition_server/faces/7769_Martha_A_58_1.jpg
Executable file
|
After Width: | Height: | Size: 67 KiB |
2
ai_face_recognition_server/requirements.txt
Normal file → Executable file
@ -3,3 +3,5 @@ flask-cors
|
|||||||
face_recognition
|
face_recognition
|
||||||
numpy
|
numpy
|
||||||
opencv-python
|
opencv-python
|
||||||
|
setuptools
|
||||||
|
gunicorn
|
||||||
|
|||||||
0
models/__init__.py
Normal file → Executable file
0
models/pos_config.py
Normal file → Executable file
0
models/res_config_settings.py
Normal file → Executable file
0
models/res_partner.py
Normal file → Executable file
0
static/src/css/face_recognition.css
Normal file → Executable file
16
static/src/js/face_recognition_sidebar.js
Normal file → Executable file
@ -65,11 +65,11 @@ export class FaceRecognitionSidebar extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture frame every 5 seconds
|
// Capture frame every 3 seconds
|
||||||
this.recognitionInterval = setInterval(() => {
|
this.recognitionInterval = setInterval(() => {
|
||||||
console.log("Recognition interval tick");
|
console.log("Recognition interval tick");
|
||||||
this.captureAndSendFrame();
|
this.captureAndSendFrame();
|
||||||
}, 5000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
async captureAndSendFrame() {
|
async captureAndSendFrame() {
|
||||||
@ -103,14 +103,19 @@ export class FaceRecognitionSidebar extends Component {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.matches && result.matches.length > 0) {
|
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
|
// 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);
|
const partner = this.pos.db.get_partner_by_id(match.id);
|
||||||
return {
|
return {
|
||||||
...match,
|
...match,
|
||||||
phone: partner ? partner.phone || partner.mobile : null
|
phone: partner ? partner.phone || partner.mobile : null
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.state.matches = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -119,13 +124,18 @@ export class FaceRecognitionSidebar extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async selectCustomer(match) {
|
async selectCustomer(match) {
|
||||||
|
console.log("selectCustomer called with match:", match);
|
||||||
const partner = this.pos.db.get_partner_by_id(match.id);
|
const partner = this.pos.db.get_partner_by_id(match.id);
|
||||||
|
console.log("Partner found in DB:", partner);
|
||||||
if (partner) {
|
if (partner) {
|
||||||
this.state.selectedCustomer = partner;
|
this.state.selectedCustomer = partner;
|
||||||
this.pos.get_order().set_partner(partner);
|
this.pos.get_order().set_partner(partner);
|
||||||
|
console.log("Partner set to current order:", this.pos.get_order().get_partner());
|
||||||
|
|
||||||
// Fetch details
|
// Fetch details
|
||||||
await this.fetchCustomerDetails(partner.id);
|
await this.fetchCustomerDetails(partner.id);
|
||||||
|
} else {
|
||||||
|
console.error("Partner not found for ID:", match.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
static/src/js/models.js
Normal file → Executable file
0
static/src/js/partner_details_edit.js
Normal file → Executable file
4
static/src/xml/face_recognition_screens.xml
Normal file → Executable file
@ -22,7 +22,7 @@
|
|||||||
<h4>Matches:</h4>
|
<h4>Matches:</h4>
|
||||||
<ul class="match-list">
|
<ul class="match-list">
|
||||||
<t t-foreach="state.matches" t-as="match" t-key="match.id">
|
<t t-foreach="state.matches" t-as="match" t-key="match.id">
|
||||||
<li t-on-click="() => this.selectCustomer(match)" class="match-item">
|
<li t-on-click="() => this.selectCustomer(match)" class="match-item" style="cursor: pointer; background: rgba(0,0,0,0.05); margin-bottom: 5px; padding: 5px;">
|
||||||
<div class="match-info">
|
<div class="match-info">
|
||||||
<div>
|
<div>
|
||||||
<span class="match-name"><t t-esc="match.name"/></span>
|
<span class="match-name"><t t-esc="match.name"/></span>
|
||||||
@ -84,7 +84,7 @@
|
|||||||
<ul t-if="state.topProducts and state.topProducts.length > 0">
|
<ul t-if="state.topProducts and state.topProducts.length > 0">
|
||||||
<t t-foreach="state.topProducts" t-as="prod" t-key="prod_index">
|
<t t-foreach="state.topProducts" t-as="prod" t-key="prod_index">
|
||||||
<li>
|
<li>
|
||||||
<t t-out="prod['name']['en_US']"/> (Qty: <t t-out="Math.round(prod['qty'])"/>)
|
<t t-out="prod['name']['en_US'] || prod['name']"/> (Qty: <t t-out="Math.round(prod['qty'])"/>)
|
||||||
</li>
|
</li>
|
||||||
</t>
|
</t>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||