AnnSankavi commited on
Commit
7d566ee
·
1 Parent(s): cad1f3d

Add Face API for Hugging Face

Browse files
Files changed (5) hide show
  1. .dockerignore +6 -0
  2. Dockerfile +13 -0
  3. README.md +122 -12
  4. main.py +282 -0
  5. requirements.txt +9 -0
.dockerignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .env
2
+ venv
3
+ __pycache__
4
+ cmake_check.txt
5
+ install_log.txt
6
+ *.bat
Dockerfile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ build-essential cmake \
5
+ && rm -rf /var/lib/apt/lists/*
6
+
7
+ WORKDIR /app
8
+ COPY . /app
9
+
10
+ RUN pip install --upgrade pip && pip install -r requirements.txt
11
+
12
+ ENV PORT=7860
13
+ CMD ["python", "main.py"]
README.md CHANGED
@@ -1,12 +1,122 @@
1
- ---
2
- title: Face Api
3
- emoji: 📊
4
- colorFrom: indigo
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
9
- short_description: Face api
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Face Verification API
2
+
3
+ A self-hosted Python backend for face enrollment and authentication, replacing FaceIO.
4
+
5
+ ## Prerequisites
6
+
7
+ - Python 3.9+
8
+ - pip (Python package manager)
9
+ - CMake and dlib dependencies (required for face_recognition)
10
+
11
+ ### Windows Setup
12
+
13
+ 1. Install Visual Studio Build Tools with C++ development tools
14
+ 2. Install CMake: `winget install cmake`
15
+
16
+ ### Linux/Mac Setup
17
+
18
+ ```bash
19
+ # Ubuntu/Debian
20
+ sudo apt-get install build-essential cmake
21
+
22
+ # macOS
23
+ brew install cmake
24
+ ```
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ cd services/face-api
30
+
31
+ # Create virtual environment
32
+ python -m venv venv
33
+
34
+ # Activate virtual environment
35
+ # Windows:
36
+ venv\Scripts\activate
37
+ # Linux/Mac:
38
+ source venv/bin/activate
39
+
40
+ # Install dependencies
41
+ pip install -r requirements.txt
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ Copy `.env.example` to `.env` and adjust settings:
47
+
48
+ ```bash
49
+ cp .env.example .env
50
+ ```
51
+
52
+ Configuration options:
53
+ - `FACE_MATCH_THRESHOLD`: Face matching strictness (0.6 recommended)
54
+ - `ALLOWED_ORIGINS`: CORS origins (comma-separated)
55
+ - `PORT`: Server port (default: 8000)
56
+
57
+ ## Running the Server
58
+
59
+ ```bash
60
+ # Development with auto-reload
61
+ python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
62
+
63
+ # Production
64
+ python main.py
65
+ ```
66
+
67
+ ## API Endpoints
68
+
69
+ ### Health Check
70
+ ```
71
+ GET /health
72
+ ```
73
+
74
+ ### Enroll Face
75
+ ```
76
+ POST /enroll
77
+ Content-Type: application/json
78
+
79
+ {
80
+ "image_base64": "base64_encoded_image",
81
+ "user_id": "user_uuid",
82
+ "metadata": {} // optional
83
+ }
84
+ ```
85
+
86
+ Response:
87
+ ```json
88
+ {
89
+ "success": true,
90
+ "face_id": "face_user_uuid_timestamp",
91
+ "face_embedding": [128 float values],
92
+ "message": "Face enrolled successfully"
93
+ }
94
+ ```
95
+
96
+ ### Verify Face
97
+ ```
98
+ POST /verify
99
+ Content-Type: application/json
100
+
101
+ {
102
+ "image_base64": "base64_encoded_image",
103
+ "stored_embedding": [128 float values],
104
+ "user_id": "user_uuid" // optional
105
+ }
106
+ ```
107
+
108
+ Response:
109
+ ```json
110
+ {
111
+ "success": true,
112
+ "match": true,
113
+ "confidence": 0.85,
114
+ "message": "Face verified successfully"
115
+ }
116
+ ```
117
+
118
+ ## Testing
119
+
120
+ ```bash
121
+ curl http://localhost:8000/health
122
+ ```
main.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Face Verification API - Python backend for face enrollment and authentication.
3
+ Replaces FaceIO with a self-hosted solution using face_recognition library.
4
+ """
5
+
6
+ import base64
7
+ import io
8
+ import os
9
+ import json
10
+ from typing import Optional, List
11
+ from contextlib import asynccontextmanager
12
+
13
+ import numpy as np
14
+ import face_recognition
15
+ from PIL import Image
16
+ from fastapi import FastAPI, HTTPException
17
+ from fastapi.middleware.cors import CORSMiddleware
18
+ from pydantic import BaseModel
19
+ from dotenv import load_dotenv
20
+
21
+ # Load environment variables
22
+ load_dotenv()
23
+
24
+ # Configuration
25
+ FACE_MATCH_THRESHOLD = float(os.getenv("FACE_MATCH_THRESHOLD", "0.6"))
26
+ ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "*").split(",")
27
+
28
+
29
+ @asynccontextmanager
30
+ async def lifespan(app: FastAPI):
31
+ """Application lifespan handler."""
32
+ print("🚀 Face Verification API starting...")
33
+ print(f" Match threshold: {FACE_MATCH_THRESHOLD}")
34
+ yield
35
+ print("👋 Face Verification API shutting down...")
36
+
37
+
38
+ app = FastAPI(
39
+ title="Face Verification API",
40
+ description="Self-hosted face enrollment and authentication service",
41
+ version="1.0.0",
42
+ lifespan=lifespan
43
+ )
44
+
45
+ # Configure CORS
46
+ app.add_middleware(
47
+ CORSMiddleware,
48
+ allow_origins=ALLOWED_ORIGINS if ALLOWED_ORIGINS[0] != "*" else ["*"],
49
+ allow_credentials=True,
50
+ allow_methods=["*"],
51
+ allow_headers=["*"],
52
+ )
53
+
54
+
55
+ # Request/Response Models
56
+ class EnrollRequest(BaseModel):
57
+ """Request model for face enrollment."""
58
+ image_base64: str
59
+ user_id: str
60
+ metadata: Optional[dict] = None
61
+
62
+
63
+ class EnrollResponse(BaseModel):
64
+ """Response model for face enrollment."""
65
+ success: bool
66
+ face_id: str
67
+ face_embedding: List[float]
68
+ message: str
69
+
70
+
71
+ class VerifyRequest(BaseModel):
72
+ """Request model for face verification."""
73
+ image_base64: str
74
+ stored_embedding: List[float]
75
+ user_id: Optional[str] = None
76
+
77
+
78
+ class VerifyResponse(BaseModel):
79
+ """Response model for face verification."""
80
+ success: bool
81
+ match: bool
82
+ confidence: float
83
+ message: str
84
+
85
+
86
+ class HealthResponse(BaseModel):
87
+ """Response model for health check."""
88
+ status: str
89
+ version: str
90
+
91
+
92
+ def decode_base64_image(base64_string: str) -> np.ndarray:
93
+ """
94
+ Decode a base64 image string to a numpy array for face_recognition.
95
+ Supports both raw base64 and data URI format.
96
+ """
97
+ # Remove data URI prefix(es) if present
98
+ # Handle cases like "data:image/jpeg;base64,data:image/png;base64,..."
99
+ if "," in base64_string:
100
+ base64_string = base64_string.split(",")[-1]
101
+
102
+ # Clean up the string and fix padding
103
+ base64_string = base64_string.strip()
104
+ padding = len(base64_string) % 4
105
+ if padding > 0:
106
+ base64_string += "=" * (4 - padding)
107
+
108
+ try:
109
+ image_bytes = base64.b64decode(base64_string)
110
+ image = Image.open(io.BytesIO(image_bytes))
111
+
112
+ # Convert to RGB if necessary (face_recognition requires RGB)
113
+ if image.mode != "RGB":
114
+ image = image.convert("RGB")
115
+
116
+ return np.array(image)
117
+ except Exception as e:
118
+ raise HTTPException(
119
+ status_code=400,
120
+ detail=f"Invalid image data: {str(e)}"
121
+ )
122
+
123
+
124
+ def extract_face_encoding(image: np.ndarray) -> np.ndarray:
125
+ """
126
+ Extract face encoding from an image.
127
+ Returns the 128-dimensional face encoding.
128
+ """
129
+ # Detect face locations
130
+ face_locations = face_recognition.face_locations(image, model="hog")
131
+
132
+ if len(face_locations) == 0:
133
+ raise HTTPException(
134
+ status_code=400,
135
+ detail="No face detected in the image. Please ensure your face is clearly visible."
136
+ )
137
+
138
+ if len(face_locations) > 1:
139
+ raise HTTPException(
140
+ status_code=400,
141
+ detail="Multiple faces detected. Please ensure only one face is visible."
142
+ )
143
+
144
+ # Extract face encoding
145
+ face_encodings = face_recognition.face_encodings(image, face_locations)
146
+
147
+ if len(face_encodings) == 0:
148
+ raise HTTPException(
149
+ status_code=400,
150
+ detail="Could not extract face features. Please try again with better lighting."
151
+ )
152
+
153
+ return face_encodings[0]
154
+
155
+
156
+ def compare_faces(stored_embedding: List[float], new_embedding: np.ndarray) -> tuple:
157
+ """
158
+ Compare two face embeddings and return match result and confidence.
159
+ """
160
+ stored_array = np.array(stored_embedding)
161
+
162
+ # Calculate face distance (lower = more similar)
163
+ face_distance = face_recognition.face_distance([stored_array], new_embedding)[0]
164
+
165
+ # Convert distance to confidence (invert so higher = more confident match)
166
+ confidence = 1.0 - face_distance
167
+
168
+ # Check if faces match based on threshold
169
+ is_match = face_distance <= FACE_MATCH_THRESHOLD
170
+
171
+ return is_match, float(confidence)
172
+
173
+
174
+ @app.get("/health", response_model=HealthResponse)
175
+ async def health_check():
176
+ """Health check endpoint."""
177
+ return HealthResponse(
178
+ status="healthy",
179
+ version="1.0.0"
180
+ )
181
+
182
+
183
+ @app.post("/enroll", response_model=EnrollResponse)
184
+ async def enroll_face(request: EnrollRequest):
185
+ """
186
+ Enroll a new face - extract and return face embedding for storage.
187
+
188
+ The embedding should be stored in the database by the client app.
189
+ """
190
+ try:
191
+ # Decode image
192
+ image = decode_base64_image(request.image_base64)
193
+
194
+ # Extract face encoding
195
+ face_encoding = extract_face_encoding(image)
196
+
197
+ # Generate a simple face ID (timestamp-based)
198
+ import time
199
+ face_id = f"face_{request.user_id}_{int(time.time() * 1000)}"
200
+
201
+ # Convert encoding to list for JSON serialization
202
+ embedding_list = face_encoding.tolist()
203
+
204
+ return EnrollResponse(
205
+ success=True,
206
+ face_id=face_id,
207
+ face_embedding=embedding_list,
208
+ message="Face enrolled successfully. Store the embedding securely."
209
+ )
210
+
211
+ except HTTPException:
212
+ raise
213
+ except Exception as e:
214
+ raise HTTPException(
215
+ status_code=500,
216
+ detail=f"Face enrollment failed: {str(e)}"
217
+ )
218
+
219
+
220
+ @app.post("/verify", response_model=VerifyResponse)
221
+ async def verify_face(request: VerifyRequest):
222
+ """
223
+ Verify a face against a stored embedding.
224
+
225
+ Returns whether the faces match and the confidence score.
226
+ """
227
+ try:
228
+ # Validate stored embedding
229
+ if not request.stored_embedding or len(request.stored_embedding) != 128:
230
+ raise HTTPException(
231
+ status_code=400,
232
+ detail="Invalid stored embedding. Expected 128-dimensional vector."
233
+ )
234
+
235
+ # Decode image
236
+ image = decode_base64_image(request.image_base64)
237
+
238
+ # Extract face encoding from new image
239
+ face_encoding = extract_face_encoding(image)
240
+
241
+ # Compare faces
242
+ is_match, confidence = compare_faces(request.stored_embedding, face_encoding)
243
+
244
+ if is_match:
245
+ message = f"Face verified successfully (confidence: {confidence:.1%})"
246
+ else:
247
+ message = f"Face verification failed - faces do not match (confidence: {confidence:.1%})"
248
+
249
+ return VerifyResponse(
250
+ success=True,
251
+ match=is_match,
252
+ confidence=confidence,
253
+ message=message
254
+ )
255
+
256
+ except HTTPException:
257
+ raise
258
+ except Exception as e:
259
+ raise HTTPException(
260
+ status_code=500,
261
+ detail=f"Face verification failed: {str(e)}"
262
+ )
263
+
264
+
265
+ @app.get("/")
266
+ async def root():
267
+ """Root endpoint with API info."""
268
+ return {
269
+ "name": "Face Verification API",
270
+ "version": "1.0.0",
271
+ "endpoints": {
272
+ "/health": "Health check",
273
+ "/enroll": "Enroll a new face (POST)",
274
+ "/verify": "Verify a face against stored embedding (POST)"
275
+ }
276
+ }
277
+
278
+
279
+ if __name__ == "__main__":
280
+ import uvicorn
281
+ port = int(os.getenv("PORT", "8000"))
282
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.109.0
2
+ uvicorn>=0.27.0
3
+ # face_recognition>=1.3.0 (Installed manually with --no-deps to use dlib-bin)
4
+ face_recognition_models>=0.3.0
5
+ numpy>=1.24.0
6
+ python-multipart>=0.0.6
7
+ httpx>=0.26.0
8
+ python-dotenv>=1.0.0
9
+ pillow>=10.0.0