File size: 6,999 Bytes
d1d65d7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
"""

Face Comparison Service using InsightFace - Hugging Face Spaces Deployment

Compares two face images and returns similarity score

"""

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import base64
import numpy as np
from io import BytesIO
from PIL import Image
from insightface.app import FaceAnalysis
import os
import logging

# Setup logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Suppress InsightFace verbose output
os.environ['INSIGHTFACE_VERBOSE'] = '0'
import warnings
warnings.filterwarnings('ignore')

app = FastAPI(
    title="Credify Face Comparison API",
    description="Face comparison using InsightFace for KYC verification",
    version="1.0.0"
)

# Initialize model (loads once at startup)
logger.info("Loading InsightFace model...")
face_app = FaceAnalysis(name="buffalo_l", providers=['CPUExecutionProvider'])
face_app.prepare(ctx_id=-1)  # -1 = CPU
logger.info("Model loaded successfully!")

# Request/Response models
class FaceComparisonRequest(BaseModel):
    aadhaarFaceImage: str  # base64 encoded image
    liveImage: str  # base64 encoded image

class FaceComparisonResponse(BaseModel):
    success: bool
    similarity: float = None
    match: bool = None
    threshold: int = None
    raw_similarity: float = None
    error: str = None

def decode_base64_image(base64_string: str):
    """Decode base64 string to PIL Image"""
    try:
        # Remove data URL prefix if present
        if ',' in base64_string:
            base64_string = base64_string.split(',')[1]
        
        # Decode base64
        image_data = base64.b64decode(base64_string)
        image = Image.open(BytesIO(image_data))
        
        # Convert to RGB if necessary
        if image.mode != 'RGB':
            image = image.convert('RGB')
        
        # Convert to numpy array
        return np.array(image)
    except Exception as e:
        logger.error(f"Error decoding image: {e}")
        raise ValueError(f"Invalid image format: {str(e)}")

def get_face_embedding(image_array):
    """Extract face embedding from image"""
    try:
        faces = face_app.get(image_array)
        
        if len(faces) == 0:
            return None
        
        # Get the first (largest) face
        face = faces[0]
        
        # Return the 512-dimensional embedding
        return face.embedding
    except Exception as e:
        logger.error(f"Error extracting face embedding: {e}")
        raise

def cosine_similarity(embedding1, embedding2):
    """Calculate cosine similarity between two embeddings"""
    try:
        # Normalize embeddings
        norm1 = np.linalg.norm(embedding1)
        norm2 = np.linalg.norm(embedding2)
        
        if norm1 == 0 or norm2 == 0:
            return 0.0
        
        # Calculate cosine similarity
        similarity = np.dot(embedding1, embedding2) / (norm1 * norm2)
        
        return float(similarity)
    except Exception as e:
        logger.error(f"Error calculating cosine similarity: {e}")
        raise

@app.post("/compare", response_model=FaceComparisonResponse)
async def compare_faces(request: FaceComparisonRequest):
    """

    Compare two face images and return similarity score

    

    - **aadhaarFaceImage**: Base64 encoded Aadhaar/ID face image

    - **liveImage**: Base64 encoded live captured face image

    

    Returns:

    - **success**: Whether comparison succeeded

    - **similarity**: Similarity percentage (0-100)

    - **match**: Whether faces match (based on threshold)

    - **threshold**: Matching threshold used

    - **error**: Error message if any

    """
    try:
        logger.info("Starting face comparison...")
        
        # Decode images
        aadhaar_image = decode_base64_image(request.aadhaarFaceImage)
        live_image = decode_base64_image(request.liveImage)
        
        # Extract face embeddings
        logger.info("Extracting embeddings from Aadhaar image...")
        aadhaar_embedding = get_face_embedding(aadhaar_image)
        
        logger.info("Extracting embeddings from live image...")
        live_embedding = get_face_embedding(live_image)
        
        # Validate embeddings
        if aadhaar_embedding is None:
            logger.warning("No face detected in Aadhaar image")
            return FaceComparisonResponse(
                success=False,
                error="No face detected in Aadhaar image. Please ensure the image clearly shows your face."
            )
        
        if live_embedding is None:
            logger.warning("No face detected in live image")
            return FaceComparisonResponse(
                success=False,
                error="No face detected in live image. Please ensure your face is clearly visible in the camera."
            )
        
        # Calculate similarity
        logger.info("Calculating similarity...")
        similarity = cosine_similarity(aadhaar_embedding, live_embedding)
        
        # Convert similarity (-1 to 1) to percentage (0-100%)
        # InsightFace embeddings typically give similarity in range 0.3-1.0 for same person
        similarity_percent = max(0, min(100, ((similarity + 1) / 2) * 100))
        
        # Threshold for face matching (typically 0.6-0.7 for InsightFace)
        # We use 75% as threshold (which corresponds to ~0.5 cosine similarity)
        threshold = 75
        is_match = similarity_percent >= threshold
        
        logger.info(f"Comparison complete: {similarity_percent}% match={is_match}")
        
        return FaceComparisonResponse(
            success=True,
            similarity=round(similarity_percent, 2),
            threshold=threshold,
            match=is_match,
            raw_similarity=round(similarity, 4)  # Raw cosine similarity for debugging
        )
        
    except Exception as e:
        logger.error(f"Face comparison failed: {e}", exc_info=True)
        return FaceComparisonResponse(
            success=False,
            error=f"Face comparison failed: {str(e)}"
        )

@app.get("/health")
async def health_check():
    """Health check endpoint"""
    return {
        "status": "healthy",
        "model": "InsightFace buffalo_l",
        "service": "Credify Face Comparison API"
    }

@app.get("/")
async def root():
    """Root endpoint - shows API info"""
    return {
        "name": "Credify Face Comparison API",
        "version": "1.0.0",
        "description": "Face comparison using InsightFace for KYC verification",
        "endpoints": {
            "POST /compare": "Compare two face images",
            "GET /health": "Health check",
            "GET /docs": "API documentation"
        }
    }

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=7860)