Spaces:
Configuration error
Configuration error
| import cv2 | |
| import numpy as np | |
| import os | |
| class IdentityRiskAnalyzer: | |
| def __init__(self): | |
| # Load Haar Cascades | |
| cascade_path = cv2.data.haarcascades | |
| self.face_cascade = cv2.CascadeClassifier(os.path.join(cascade_path, 'haarcascade_frontalface_default.xml')) | |
| self.eye_cascade = cv2.CascadeClassifier(os.path.join(cascade_path, 'haarcascade_eye.xml')) | |
| # Smile cascade is often less reliable, but we can try if available, or skip expression strictness. | |
| # self.smile_cascade = cv2.CascadeClassifier(os.path.join(cascade_path, 'haarcascade_smile.xml')) | |
| def analyze(self, pil_image): | |
| """ | |
| Analyzes a PIL Image for identity theft risk using OpenCV. | |
| """ | |
| # Convert PIL to CV2 (BGR) | |
| img_np = np.array(pil_image) | |
| if img_np.shape[2] == 4: # RGBA to RGB | |
| img_np = cv2.cvtColor(img_np, cv2.COLOR_RGBA2RGB) | |
| # OpenCV expects BGR | |
| img_bgr = cv2.cvtColor(img_np, cv2.COLOR_RGB2BGR) | |
| gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) | |
| h, w = gray.shape | |
| results = { | |
| "is_high_risk": False, | |
| "risk_score": 0.0, | |
| "details": [], | |
| "passed_criteria": [] | |
| } | |
| # 1. Face Visibility & Count | |
| faces = self.face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)) | |
| if len(faces) == 0: | |
| results["details"].append("No face detected.") | |
| return results | |
| if len(faces) > 1: | |
| results["details"].append("Multiple faces detected.") | |
| return results | |
| results["passed_criteria"].append("Single face visible") | |
| # Get Face ROI | |
| (x, y, fw, fh) = faces[0] | |
| face_roi_gray = gray[y:y+fh, x:x+fw] | |
| face_ratio = (fw * fh) / (w * h) | |
| if face_ratio < 0.05: | |
| results["details"].append("Face too small relative to image.") | |
| # 2. Alignment (Eyes) | |
| eyes = self.eye_cascade.detectMultiScale(face_roi_gray) | |
| if len(eyes) >= 2: | |
| # Sort by x position to get left and right eye | |
| eyes = sorted(eyes, key=lambda e: e[0]) | |
| (ex1, ey1, ew1, eh1) = eyes[0] | |
| (ex2, ey2, ew2, eh2) = eyes[-1] # Farthest right | |
| # Check Angle (Roll) | |
| dy = (ey2 + eh2/2) - (ey1 + eh1/2) | |
| dx = (ex2 + ew2/2) - (ex1 + ew1/2) | |
| angle = np.degrees(np.arctan2(dy, dx)) | |
| if abs(angle) > 10: | |
| results["details"].append(f"Face tilted (Angle: {angle:.1f}°).") | |
| else: | |
| results["passed_criteria"].append("Face vertically aligned") | |
| # Check Centering (Yaw/Translation) | |
| face_center_x = x + fw/2 | |
| img_center_x = w/2 | |
| deviation = abs(face_center_x - img_center_x) | |
| if deviation > w * 0.15: | |
| results["details"].append("Face not centered.") | |
| else: | |
| results["passed_criteria"].append("Face centered") | |
| else: | |
| # Can't see both eyes -> Maybe side profile or hair? | |
| # For High Risk ID, we NEED eyes visible. | |
| results["details"].append("Eyes not clearly visible or aligned.") | |
| # 3. Image Quality (Blur) | |
| laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() | |
| if laplacian_var < 50: | |
| results["details"].append("Image too blurry.") | |
| else: | |
| results["passed_criteria"].append("High resolution/sharp") | |
| # 4. Lighting & Background | |
| margin = int(min(w, h) * 0.1) | |
| if margin < 1: margin = 1 | |
| # Check top corners for background uniformity | |
| top_strip = gray[0:margin, :] | |
| bg_var = np.var(top_strip) | |
| bg_mean = np.mean(top_strip) | |
| if bg_var > 2500: # Relaxed threshold for "real world" plain walls | |
| results["details"].append("Background not plain/uniform.") | |
| elif bg_mean < 80: # Too dark | |
| results["details"].append("Background too dark.") | |
| else: | |
| results["passed_criteria"].append("Plain light-colored background") | |
| # 5. Expression (Heuristic) | |
| # Without landmarks, checking "neutral" is hard. | |
| # But we can skip strict Smile check as user requested leniency. | |
| # We assume if it passes eye alignment and is frontal, it's risky enough. | |
| results["passed_criteria"].append("Expression check skipped (Lenient)") | |
| # Final Scoring | |
| # High Risk if NO details (failures). | |
| if len(results["details"]) == 0: | |
| results["is_high_risk"] = True | |
| results["risk_score"] = 0.95 | |
| else: | |
| # Special Case: If only Background failed? No, ID photo needs plain BG. | |
| results["is_high_risk"] = False | |
| return results | |