from flask import Flask, request, jsonify import cv2 import mediapipe as mp import numpy as np from PIL import Image import math import io import base64 import requests import os import threading import time from datetime import datetime import traceback try: from transformers import pipeline TRANSFORMERS_AVAILABLE = True print("[AI] ✓ Transformers available") except ImportError: TRANSFORMERS_AVAILABLE = False print("[AI] ✗ Transformers not available") FACE_PARSER = None FACE_PARSING_AVAILABLE = False FACE_PARSING_LABELS = { 0: 'background', 1: 'skin', 2: 'nose', # ← اصلاح شد 3: 'eye_g', # ← eyeglasses 4: 'l_eye', 5: 'r_eye', 6: 'l_brow', # ← اصلاح شد 7: 'r_brow', # ← اصلاح شد 8: 'l_ear', # ← اصلاح شد 9: 'r_ear', # ← اصلاح شد 10: 'mouth', # ← اصلاح شد 11: 'u_lip', # ← اصلاح شد 12: 'l_lip', # ← اصلاح شد 13: 'hair', # ← اصلاح شد 14: 'hat', # ← اصلاح شد 15: 'ear_r', # ← earring - اصلاح شد 16: 'neck_l', # ← necklace - اصلاح شد 17: 'neck', # ← اصلاح شد 18: 'cloth' # ← اصلاح شد } LABEL_COLORS = { 0: (0, 0, 0), # background - مشکی 1: (204, 0, 0), # skin - قرمز تیره 2: (76, 153, 0), # nose - سبز تیره 🟢 3: (255, 0, 0), # eye_g (eyeglasses) - قرمز 🔴 4: (51, 51, 255), # l_eye - آبی 5: (0, 255, 255), # r_eye - فیروزه‌ای 6: (255, 255, 0), # l_brow - زرد 7: (204, 102, 0), # r_brow - قهوه‌ای مایل به نارنجی 🟤 8: (153, 0, 76), # l_ear - بنفش تیره 9: (255, 102, 153), # r_ear - صورتی مایل به نارنجی 🌸 10: (102, 255, 153), # mouth - سبز روشن 11: (255, 0, 255), # u_lip - صورتی 12: (204, 0, 153), # l_lip - بنفش صورتی 💜 13: (0, 204, 204), # hair - فیروزه‌ای تیره 14: (0, 255, 0), # hat - سبز روشن 🟢 15: (255, 204, 0), # ear_r (earring) - نارنجی روشن 16: (204, 0, 204), # neck_l (necklace) - صورتی تیره 17: (255, 153, 51), # neck - نارنجی 18: (102, 102, 156) # cloth - آبی خاکستری } def init_face_parser(): global FACE_PARSER, FACE_PARSING_AVAILABLE if FACE_PARSING_AVAILABLE: return True try: print("[FaceParsing] Loading face-parsing model...") from transformers import AutoImageProcessor, AutoModelForSemanticSegmentation model_name = "jonathandinu/face-parsing" print(f"[FaceParsing] Loading from {model_name}...") processor = AutoImageProcessor.from_pretrained(model_name) model = AutoModelForSemanticSegmentation.from_pretrained(model_name) FACE_PARSER = { 'processor': processor, 'model': model } FACE_PARSING_AVAILABLE = True print("[FaceParsing] ✓ Model loaded successfully!") return True except Exception as e1: print(f"[FaceParsing] Method 1 failed: {e1}") try: print("[FaceParsing] Trying ONNX version...") import onnxruntime as ort model_path = "face_parsing.onnx" if os.path.exists(model_path): session = ort.InferenceSession(model_path) FACE_PARSER = {'session': session, 'type': 'onnx'} FACE_PARSING_AVAILABLE = True print("[FaceParsing] ✓ ONNX model loaded!") return True else: print("[FaceParsing] ONNX file not found") except Exception as e2: print(f"[FaceParsing] Method 2 failed: {e2}") print("[FaceParsing] ⚠ Will use CV2 fallback methods") return False def predict_face_parsing(image_pil): global FACE_PARSER if not FACE_PARSING_AVAILABLE or FACE_PARSER is None: return None try: import torch if 'processor' in FACE_PARSER: processor = FACE_PARSER['processor'] model = FACE_PARSER['model'] inputs = processor(images=image_pil, return_tensors="pt") with torch.no_grad(): outputs = model(**inputs) logits = outputs.logits h, w = image_pil.size[1], image_pil.size[0] upsampled_logits = torch.nn.functional.interpolate( logits, size=(h, w), mode="bilinear", align_corners=False ) parsing_mask = upsampled_logits.argmax(dim=1)[0].cpu().numpy() return parsing_mask.astype(np.uint8) elif 'session' in FACE_PARSER: session = FACE_PARSER['session'] img_array = np.array(image_pil.resize((512, 512))) img_array = img_array.transpose(2, 0, 1) # HWC -> CHW img_array = img_array.astype(np.float32) / 255.0 img_array = np.expand_dims(img_array, 0) # Add batch dim outputs = session.run(None, {'input': img_array}) parsing_mask = outputs[0].argmax(axis=1)[0] h, w = image_pil.size[1], image_pil.size[0] parsing_mask = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) return parsing_mask return None except Exception as e: print(f"[FaceParsing] Prediction error: {e}") import traceback traceback.print_exc() return None def predict_face_parsing_xenova(image_pil): global FACE_PARSER if not FACE_PARSING_AVAILABLE or FACE_PARSER is None: return None try: output = FACE_PARSER(image_pil) h, w = image_pil.size[1], image_pil.size[0] parsing_mask = np.zeros((h, w), dtype=np.uint8) for item in output: label_name = item['label'] mask_pil = item['mask'] mask_np = np.array(mask_pil) if len(mask_np.shape) == 3: mask_np = cv2.cvtColor(mask_np, cv2.COLOR_RGB2GRAY) if mask_np.shape != (h, w): mask_np = cv2.resize(mask_np, (w, h), interpolation=cv2.INTER_NEAREST) label_id = XENOVA_LABELS.get(label_name, 0) parsing_mask[mask_np > 127] = label_id return parsing_mask except Exception as e: print(f"[FaceParsing] Error: {e}") return None def create_colored_mask(parsing_mask): h, w = parsing_mask.shape colored = np.zeros((h, w, 3), dtype=np.uint8) for label_id, color in LABEL_COLORS.items(): colored[parsing_mask == label_id] = color return colored def create_transparent_overlay(original_pil, parsing_mask, alpha=0.5): h, w = parsing_mask.shape original_resized = original_pil.resize((w, h)) original_np = np.array(original_resized) colored = create_colored_mask(parsing_mask) return (original_np * (1 - alpha) + colored * alpha).astype(np.uint8) def add_legend_to_image(image, active_labels): img = image.copy() h, w = img.shape[:2] important_labels = { 1: {'name': 'Skin', 'color': LABEL_COLORS[1]}, 2: {'name': 'Nose', 'color': LABEL_COLORS[2]}, 3: {'name': 'Eyeglasses', 'color': LABEL_COLORS[3]}, 4: {'name': 'L Eye', 'color': LABEL_COLORS[4]}, 5: {'name': 'R Eye', 'color': LABEL_COLORS[5]}, 6: {'name': 'L Brow', 'color': LABEL_COLORS[6]}, 7: {'name': 'R Brow', 'color': LABEL_COLORS[7]}, 8: {'name': 'L Ear', 'color': LABEL_COLORS[8]}, 9: {'name': 'R Ear', 'color': LABEL_COLORS[9]}, 10: {'name': 'Mouth', 'color': LABEL_COLORS[10]}, 11: {'name': 'U Lip', 'color': LABEL_COLORS[11]}, 12: {'name': 'L Lip', 'color': LABEL_COLORS[12]}, 13: {'name': 'Hair', 'color': LABEL_COLORS[13]}, 14: {'name': 'Hat', 'color': LABEL_COLORS[14]}, 15: {'name': 'Earring', 'color': LABEL_COLORS[15]}, 16: {'name': 'Necklace', 'color': LABEL_COLORS[16]}, 17: {'name': 'Neck', 'color': LABEL_COLORS[17]}, 18: {'name': 'Cloth', 'color': LABEL_COLORS[18]} } legend_items = [] for label_id in active_labels: if label_id in important_labels and label_id != 0: legend_items.append({ 'id': label_id, 'name': important_labels[label_id]['name'], 'color': important_labels[label_id]['color'] }) if not legend_items: return img priority_order = [3, 14, 15, 16, 13, 4, 5, 2, 10, 18, 1, 17, 8, 9, 6, 7, 11, 12] legend_items.sort(key=lambda x: priority_order.index(x['id']) if x['id'] in priority_order else 999) base_size = min(w, h) if base_size < 400: font_scale = 0.25 thickness = 1 line_height = 15 box_size = 10 padding = 4 title_scale = 0.25 title_line_height = 12 title_bottom_margin = 10 # ✅ فضای خالی اضافه زیر تایتل elif base_size < 600: font_scale = 0.3 thickness = 1 line_height = 18 box_size = 12 padding = 5 title_scale = 0.3 title_line_height = 14 title_bottom_margin = 12 # ✅ فضای خالی اضافه زیر تایتل elif base_size < 800: font_scale = 0.4 thickness = 1 line_height = 20 box_size = 14 padding = 6 title_scale = 0.4 title_line_height = 16 title_bottom_margin = 14 # ✅ فضای خالی اضافه زیر تایتل else: font_scale = 0.5 thickness = 1 line_height = 22 box_size = 16 padding = 7 title_scale = 0.5 title_line_height = 18 title_bottom_margin = 16 # ✅ فضای خالی اضافه زیر تایتل num_items = len(legend_items) mid_point = (num_items + 1) // 2 left_column_items = legend_items[:mid_point] # ستون چپ right_column_items = legend_items[mid_point:] # ستون راست def draw_legend_column(items, side='A'): """ side: 'A' or 'B' """ if not items: return max_text_width = 0 for item in items: (text_w, text_h), _ = cv2.getTextSize(item['name'], cv2.FONT_HERSHEY_SIMPLEX, font_scale, thickness) max_text_width = max(max_text_width, text_w) title_line1 = "Detected" title_line2 = f"({side}):" (title1_w, title1_h), _ = cv2.getTextSize(title_line1, cv2.FONT_HERSHEY_SIMPLEX, title_scale, thickness ) (title2_w, title2_h), _ = cv2.getTextSize(title_line2, cv2.FONT_HERSHEY_SIMPLEX, title_scale, thickness ) max_title_width = max(title1_w, title2_w) column_w = max(box_size + padding * 3 + max_text_width, max_title_width + padding * 2) column_w = min(column_w, int(w * 0.25)) # حداکثر 25% عرض تصویر column_h = (padding * 2 + title_line_height * 2 + # دو خط عنوان title_bottom_margin + # ✅ فضای خالی اضافه زیر تایتل len(items) * line_height + padding) if side == 'A': column_x = padding * 2 else: # 'B' column_x = w - column_w - padding * 2 column_y = padding * 2 if column_x + column_w > w: column_x = w - column_w - padding if column_y + column_h > h: column_y = h - column_h - padding overlay = img.copy() cv2.rectangle(overlay, (column_x, column_y), (column_x + column_w, column_y + column_h), (0, 0, 0), -1) cv2.addWeighted(overlay, 0.75, img, 0.25, 0, img) cv2.rectangle(img, (column_x, column_y), (column_x + column_w, column_y + column_h), (255, 255, 255), 1) title_start_y = column_y + padding + title_line_height title1_x = column_x + (column_w - title1_w) // 2 cv2.putText(img, title_line1, (title1_x, title_start_y), cv2.FONT_HERSHEY_SIMPLEX, title_scale, (255, 255, 255), thickness ) title2_x = column_x + (column_w - title2_w) // 2 cv2.putText(img, title_line2, (title2_x, title_start_y + title_line_height), cv2.FONT_HERSHEY_SIMPLEX, title_scale, (255, 255, 255), thickness ) start_y = title_start_y + title_line_height + title_bottom_margin for idx, item in enumerate(items): y_pos = start_y + idx * line_height if y_pos + line_height > column_y + column_h - padding: break box_y = y_pos - box_size // 2 cv2.rectangle(img, (column_x + padding, box_y), (column_x + padding + box_size, box_y + box_size), item['color'], -1) cv2.rectangle(img, (column_x + padding, box_y), (column_x + padding + box_size, box_y + box_size), (255, 255, 255), 1) text_x = column_x + padding * 2 + box_size cv2.putText(img, item['name'], (text_x, y_pos), cv2.FONT_HERSHEY_SIMPLEX, font_scale, (255, 255, 255), thickness) draw_legend_column(left_column_items, side='A') draw_legend_column(right_column_items, side='B') return img class PassportPhotoProcessor: def __init__(self): self.mp_face_mesh = mp.solutions.face_mesh self.mp_pose = mp.solutions.pose self.face_mesh = self.mp_face_mesh.FaceMesh( static_image_mode=True, max_num_faces=1, refine_landmarks=True, min_detection_confidence=0.5 ) self.pose = self.mp_pose.Pose( static_image_mode=True, model_complexity=2, min_detection_confidence=0.5 ) def detect_landmarks(self, image): rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) face_results = self.face_mesh.process(rgb_image) pose_results = self.pose.process(rgb_image) if not face_results.multi_face_landmarks: raise ValueError("No face detected in the image") return face_results.multi_face_landmarks[0], pose_results def get_eye_centers(self, landmarks, img_width, img_height): left_eye_indices = [33, 133, 160, 159, 158, 157, 173] right_eye_indices = [362, 263, 387, 386, 385, 384, 398] left_eye_x = np.mean([landmarks.landmark[i].x for i in left_eye_indices]) * img_width left_eye_y = np.mean([landmarks.landmark[i].y for i in left_eye_indices]) * img_height right_eye_x = np.mean([landmarks.landmark[i].x for i in right_eye_indices]) * img_width right_eye_y = np.mean([landmarks.landmark[i].y for i in right_eye_indices]) * img_height return (left_eye_x, left_eye_y), (right_eye_x, right_eye_y) def get_nose_tip(self, landmarks, img_width, img_height): nose_tip = landmarks.landmark[4] return nose_tip.x * img_width, nose_tip.y * img_height def get_chin(self, landmarks, img_width, img_height): chin = landmarks.landmark[152] return chin.x * img_width, chin.y * img_height def get_forehead_top(self, landmarks, img_width, img_height): forehead_indices = [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288, 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136, 172, 58, 132, 93, 234, 127, 162, 21, 54, 103, 67, 109] min_y = min([landmarks.landmark[i].y for i in forehead_indices]) * img_height avg_x = np.mean([landmarks.landmark[i].x for i in forehead_indices]) * img_width chin_y = landmarks.landmark[152].y * img_height eye_indices = [33, 133, 362, 263] eye_y = np.mean([landmarks.landmark[i].y for i in eye_indices]) * img_height face_height = chin_y - eye_y hair_extension = face_height * 0.65 estimated_hair_top = max(0, min_y - hair_extension) return avg_x, estimated_hair_top def get_shoulders(self, pose_results, img_width, img_height): if not pose_results.pose_landmarks: return None left_shoulder = pose_results.pose_landmarks.landmark[11] right_shoulder = pose_results.pose_landmarks.landmark[12] return { 'left': (left_shoulder.x * img_width, left_shoulder.y * img_height), 'right': (right_shoulder.x * img_width, right_shoulder.y * img_height) } def calculate_rotation_angle(self, left_eye, right_eye): dx = right_eye[0] - left_eye[0] dy = right_eye[1] - left_eye[1] angle = math.degrees(math.atan2(dy, dx)) return angle def rotate_image(self, image, angle, center): h, w = image.shape[:2] matrix = cv2.getRotationMatrix2D(center, angle, 1.0) rotated = cv2.warpAffine(image, matrix, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) return rotated, matrix def rotate_point(self, point, matrix): px, py = point new_x = matrix[0,0]*px + matrix[0,1]*py + matrix[0,2] new_y = matrix[1,0]*px + matrix[1,1]*py + matrix[1,2] return (new_x, new_y) def calculate_crop_box(self, image, eye_line_y, chin_y, top_hair_y, nose_x, shoulders, body_bottom_y): h, w = image.shape[:2] EYE_TO_BOTTOM_MIN = 0.56 EYE_TO_BOTTOM_MAX = 0.69 FACE_HEIGHT_MIN = 0.50 FACE_HEIGHT_MAX = 0.69 face_height = chin_y - top_hair_y best_crop = None best_score = float('inf') eye_to_bottom = body_bottom_y - eye_line_y min_crop_from_eye = eye_to_bottom / EYE_TO_BOTTOM_MAX max_crop_from_eye = eye_to_bottom / EYE_TO_BOTTOM_MIN min_crop_from_face = face_height / FACE_HEIGHT_MAX max_crop_from_face = face_height / FACE_HEIGHT_MIN min_crop_size = max(min_crop_from_face, min_crop_from_eye, 600) max_crop_size = min(max_crop_from_face, max_crop_from_eye, h, w) print(f"[CropBox] Eye to bottom: {eye_to_bottom:.0f}px") print(f"[CropBox] Face height: {face_height:.0f}px") print(f"[CropBox] Min from eye: {min_crop_from_eye:.0f}, Max: {max_crop_from_eye:.0f}") print(f"[CropBox] Min from face: {min_crop_from_face:.0f}, Max: {max_crop_from_face:.0f}") print(f"[CropBox] Final range: {min_crop_size:.0f} - {max_crop_size:.0f}") if max_crop_size < 600: raise ValueError("Image too small to create passport photo") if min_crop_size > max_crop_size: print(f"[CropBox] Constraint conflict detected. Using flexible approach...") target_size = (min_crop_from_eye + max_crop_from_eye) / 2 target_size = max(600, min(1200, target_size, h, w)) min_crop_size = max(600, target_size * 0.85) max_crop_size = min(1200, target_size * 1.15, h, w) print(f"[CropBox] Adjusted range: {min_crop_size:.0f} - {max_crop_size:.0f}") if max_crop_size < 600: raise ValueError("Image too small to create passport photo") if min_crop_size > max_crop_size: min_crop_size = max_crop_size = target_size print(f"[CropBox] Using fixed size: {target_size:.0f}") search_steps = max(50, int((max_crop_size - min_crop_size) / 10)) for size in np.linspace(max_crop_size, min_crop_size, search_steps): size = int(size) target_eye_ratio = (EYE_TO_BOTTOM_MIN + EYE_TO_BOTTOM_MAX) / 2 top = eye_line_y - (size * (1 - target_eye_ratio)) if top > top_hair_y - (size * 0.05): top = top_hair_y - (size * 0.05) if top + size < chin_y + (size * 0.05): top = chin_y + (size * 0.05) - size left = nose_x - size / 2 right = left + size bottom = top + size if left < 0: left = 0 right = size if right > w: right = w left = w - size if top < 0: top = 0 bottom = size if bottom > h: bottom = h top = h - size if shoulders: shoulder_width = abs(shoulders['right'][0] - shoulders['left'][0]) if shoulder_width > size * 0.95: continue shoulder_left = min(shoulders['left'][0], shoulders['right'][0]) shoulder_right = max(shoulders['left'][0], shoulders['right'][0]) if shoulder_left < left + (size * 0.025) or shoulder_right > right - (size * 0.025): shoulder_center = (shoulder_left + shoulder_right) / 2 left = shoulder_center - size / 2 right = left + size if left < 0 or right > w: continue eye_to_bottom_ratio = (bottom - eye_line_y) / size face_height_ratio = (chin_y - top_hair_y) / size eye_ok = EYE_TO_BOTTOM_MIN <= eye_to_bottom_ratio <= EYE_TO_BOTTOM_MAX face_ok = FACE_HEIGHT_MIN <= face_height_ratio <= FACE_HEIGHT_MAX score = 0 eye_deviation = abs(eye_to_bottom_ratio - target_eye_ratio) if not eye_ok: score += eye_deviation * 800 else: score += eye_deviation * 100 target_face_ratio = (FACE_HEIGHT_MIN + FACE_HEIGHT_MAX) / 2 face_deviation = abs(face_height_ratio - target_face_ratio) if not face_ok: score += face_deviation * 400 else: score += face_deviation * 50 score += (1200 - size) * 0.5 if score < best_score: best_score = score best_crop = { 'left': int(left), 'top': int(top), 'right': int(right), 'bottom': int(bottom), 'size': size, 'eye_to_bottom_ratio': eye_to_bottom_ratio * 100, 'face_height_ratio': face_height_ratio * 100, 'score': score } if eye_ok and (face_ok or face_deviation < 0.05): print(f"[CropBox] Found good solution at size {size:.0f}") break if not best_crop: raise ValueError("Could not create suitable crop. Please ensure photo shows full head and shoulders.") print(f"[CropBox] Best crop: {best_crop['size']:.0f}px, " + f"eye={best_crop['eye_to_bottom_ratio']:.1f}%, " + f"face={best_crop['face_height_ratio']:.1f}%, " + f"score={best_crop['score']:.1f}") return best_crop def compress_to_size(self, image, max_kb=240): h, w = image.shape[:2] if h < 600 or w < 600: raise ValueError("Image size too small (minimum 600x600px)") if h > 1200 or w > 1200: print(f"[Compress] Original size: {w}x{h}, resizing to max 1200px") scale = 1200 / max(h, w) new_w = int(w * scale) new_h = int(h * scale) image = cv2.resize(image, (new_w, new_h), interpolation=cv2.INTER_AREA) h, w = new_h, new_w print(f"[Compress] Resized to: {w}x{h}") original_image = image.copy() original_h, original_w = h, w pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) buffer = io.BytesIO() pil_image.save(buffer, format='JPEG', quality=100, optimize=True) size_kb = buffer.tell() / 1024 print(f"[Compress] Initial size at quality 100: {size_kb:.2f} KB") if size_kb <= max_kb: print(f"[Compress] ✓ Already under {max_kb}KB") return buffer.getvalue(), size_kb, 100, (w, h) current_size = w best_quality = 100 print(f"[Compress] Starting FAST reduction (step: 100px)") while current_size > 600 and size_kb > max_kb * 1.5: current_size -= 100 current_size = max(600, current_size) new_size = (current_size, current_size) resized = cv2.resize(original_image, new_size, interpolation=cv2.INTER_AREA) pil_image = Image.fromarray(cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)) buffer = io.BytesIO() pil_image.save(buffer, format='JPEG', quality=best_quality, optimize=True) size_kb = buffer.tell() / 1024 print(f"[Compress] FAST - Size {current_size}px, quality {best_quality}: {size_kb:.2f} KB") if size_kb <= max_kb: print(f"[Compress] ✓ Target reached at {current_size}px") return buffer.getvalue(), size_kb, best_quality, (current_size, current_size) print(f"[Compress] Starting SLOW reduction (step: 10px)") while current_size > 600 and size_kb > max_kb: current_size -= 10 current_size = max(600, current_size) new_size = (current_size, current_size) resized = cv2.resize(original_image, new_size, interpolation=cv2.INTER_AREA) pil_image = Image.fromarray(cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)) buffer = io.BytesIO() pil_image.save(buffer, format='JPEG', quality=best_quality, optimize=True) size_kb = buffer.tell() / 1024 print(f"[Compress] SLOW - Size {current_size}px, quality {best_quality}: {size_kb:.2f} KB") if size_kb <= max_kb: print(f"[Compress] ✓ Target reached at {current_size}px") return buffer.getvalue(), size_kb, best_quality, (current_size, current_size) print(f"[Compress] Starting QUALITY reduction (step: 1)") current_quality = 100 best_size = current_size while current_quality >= 50 and size_kb > max_kb: new_size = (current_size, current_size) resized = cv2.resize(original_image, new_size, interpolation=cv2.INTER_AREA) pil_image = Image.fromarray(cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)) buffer = io.BytesIO() pil_image.save(buffer, format='JPEG', quality=current_quality, optimize=True) size_kb = buffer.tell() / 1024 print(f"[Compress] QUALITY - Size {current_size}px, quality {current_quality}: {size_kb:.2f} KB") if size_kb <= max_kb: best_quality = current_quality best_size = current_size print(f"[Compress] ✓ Found acceptable quality {best_quality} at size {best_size}") break current_quality -= 1 if size_kb <= max_kb: print(f"[Compress] Starting SIZE OPTIMIZATION (step: +5px)") optimized_size = best_size optimized_buffer = buffer while optimized_size < original_w and optimized_size < 1200: test_size = optimized_size + 5 new_size = (test_size, test_size) resized = cv2.resize(original_image, new_size, interpolation=cv2.INTER_AREA) pil_image = Image.fromarray(cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)) test_buffer = io.BytesIO() pil_image.save(test_buffer, format='JPEG', quality=best_quality, optimize=True) test_size_kb = test_buffer.tell() / 1024 if test_size_kb <= max_kb: optimized_size = test_size optimized_buffer = test_buffer size_kb = test_size_kb print(f"[Compress] OPTIMIZE - Size {optimized_size}px, quality {best_quality}: {size_kb:.2f} KB") else: print(f"[Compress] OPTIMIZE - Size {test_size}px exceeds limit: {test_size_kb:.2f} KB") break print(f"[Compress] ✓ Optimized to size {optimized_size}px with quality {best_quality}") return optimized_buffer.getvalue(), size_kb, best_quality, (optimized_size, optimized_size) print(f"[Compress] ⚠️ Could not reach {max_kb}KB, returning at {size_kb:.2f}KB") return buffer.getvalue(), size_kb, current_quality, (current_size, current_size) def create_analysis_image(self, image, eye_line_y, chin_y, top_hair_y, nose_x, eye_to_bottom_ratio, face_height_ratio): analysis_img = image.copy() h, w = analysis_img.shape[:2] GREEN = (0, 255, 0) # Eye line BLUE = (255, 0, 0) # Top of hair RED = (0, 0, 255) # Chin YELLOW = (0, 255, 255) # Nose center CYAN = (255, 255, 0) # Eye to Bottom (cyan) MAGENTA = (255, 0, 255) # Face Height (magenta) WHITE = (255, 255, 255) BLACK = (0, 0, 0) cv2.line(analysis_img, (0, int(top_hair_y)), (w, int(top_hair_y)), BLUE, 2) cv2.line(analysis_img, (0, int(eye_line_y)), (w, int(eye_line_y)), GREEN, 2) cv2.line(analysis_img, (0, int(chin_y)), (w, int(chin_y)), RED, 2) cv2.line(analysis_img, (int(nose_x), 0), (int(nose_x), h), YELLOW, 2) eye_bottom_x = int(w * 0.15) cv2.line(analysis_img, (eye_bottom_x, int(eye_line_y)), (eye_bottom_x, h), CYAN, 3) arrow_size = 15 cv2.arrowedLine(analysis_img, (eye_bottom_x, int(eye_line_y) + 30), (eye_bottom_x, int(eye_line_y)), CYAN, 2, tipLength=0.3) cv2.arrowedLine(analysis_img, (eye_bottom_x, h - 30), (eye_bottom_x, h - 1), CYAN, 2, tipLength=0.3) text = f"Eye to Bottom: {eye_to_bottom_ratio:.1f}%" text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0] text_x = eye_bottom_x - text_size[0] // 2 text_y = int((eye_line_y + h) / 2) cv2.rectangle(analysis_img, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), BLACK, -1) cv2.putText(analysis_img, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, CYAN, 2) face_height_x = int(w * 0.85) cv2.line(analysis_img, (face_height_x, int(top_hair_y)), (face_height_x, int(chin_y)), MAGENTA, 3) cv2.arrowedLine(analysis_img, (face_height_x, int(top_hair_y) + 30), (face_height_x, int(top_hair_y)), MAGENTA, 2, tipLength=0.3) cv2.arrowedLine(analysis_img, (face_height_x, int(chin_y) - 30), (face_height_x, int(chin_y)), MAGENTA, 2, tipLength=0.3) text = f"Face Height: {face_height_ratio:.1f}%" text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, 0.6, 2)[0] text_x = face_height_x - text_size[0] // 2 text_y = int((top_hair_y + chin_y) / 2) cv2.rectangle(analysis_img, (text_x - 5, text_y - text_size[1] - 5), (text_x + text_size[0] + 5, text_y + 5), BLACK, -1) cv2.putText(analysis_img, text, (text_x, text_y), cv2.FONT_HERSHEY_SIMPLEX, 0.6, MAGENTA, 2) legend_x = w - 250 legend_y = 30 line_height = 25 overlay = analysis_img.copy() cv2.rectangle(overlay, (legend_x - 10, legend_y - 10), (w - 10, legend_y + line_height * 5 + 10), BLACK, -1) cv2.addWeighted(overlay, 0.7, analysis_img, 0.3, 0, analysis_img) legends = [ ("Top Hair", BLUE), ("Eye Line", GREEN), ("Chin", RED), ("Nose Center", YELLOW), ("Eye-Bottom", CYAN), ("Face Height", MAGENTA) ] for idx, (label, color) in enumerate(legends): y_pos = legend_y + idx * line_height cv2.line(analysis_img, (legend_x, y_pos), (legend_x + 30, y_pos), color, 2) cv2.putText(analysis_img, label, (legend_x + 40, y_pos + 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, WHITE, 1) return analysis_img def process_image_from_base64(self, image_base64): try: image_bytes = base64.b64decode(image_base64) nparr = np.frombuffer(image_bytes, np.uint8) image = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if image is None: raise ValueError("Failed to decode image") h, w = image.shape[:2] face_landmarks, pose_results = self.detect_landmarks(image) left_eye, right_eye = self.get_eye_centers(face_landmarks, w, h) angle = self.calculate_rotation_angle(left_eye, right_eye) center = ((left_eye[0] + right_eye[0]) / 2, (left_eye[1] + right_eye[1]) / 2) rotated_image, rotation_matrix = self.rotate_image(image, angle, center) left_eye = self.rotate_point(left_eye, rotation_matrix) right_eye = self.rotate_point(right_eye, rotation_matrix) eye_line_y = (left_eye[1] + right_eye[1]) / 2 face_landmarks, pose_results = self.detect_landmarks(rotated_image) nose_x, nose_y = self.get_nose_tip(face_landmarks, w, h) chin_x, chin_y = self.get_chin(face_landmarks, w, h) hair_x, top_hair_y = self.get_forehead_top(face_landmarks, w, h) shoulders = self.get_shoulders(pose_results, w, h) body_bottom_y = h if shoulders: body_bottom_y = max(shoulders['left'][1], shoulders['right'][1]) crop_box = self.calculate_crop_box( rotated_image, eye_line_y, chin_y, top_hair_y, nose_x, shoulders, body_bottom_y ) cropped = rotated_image[ crop_box['top']:crop_box['bottom'], crop_box['left']:crop_box['right'] ] analysis_image = self.create_analysis_image( cropped, eye_line_y - crop_box['top'], chin_y - crop_box['top'], top_hair_y - crop_box['top'], nose_x - crop_box['left'], crop_box['eye_to_bottom_ratio'], # اضافه شده crop_box['face_height_ratio'] # اضافه شده ) final_bytes, file_size, quality, final_size = self.compress_to_size(cropped, max_kb=240) final_image_b64 = base64.b64encode(final_bytes).decode('utf-8') _, analysis_buffer = cv2.imencode('.jpg', analysis_image, [cv2.IMWRITE_JPEG_QUALITY, 95]) analysis_image_b64 = base64.b64encode(analysis_buffer).decode('utf-8') return { 'service_type': 'processing', 'final_image': final_image_b64, 'analysis_image': analysis_image_b64, 'info': { 'size': f"{final_size[0]}x{final_size[1]}", 'file_size': f"{file_size:.2f} KB", 'quality': quality, 'eye_to_bottom': f"{crop_box['eye_to_bottom_ratio']:.1f}%", 'face_height': f"{crop_box['face_height_ratio']:.1f}%" } } except Exception as e: raise Exception(f"Processing error: {str(e)}") class PhotoRequirementsChecker: def __init__(self): self.mp_face_mesh = mp.solutions.face_mesh self.mp_face_detection = mp.solutions.face_detection self.face_mesh = self.mp_face_mesh.FaceMesh( static_image_mode=True, max_num_faces=2, refine_landmarks=True, min_detection_confidence=0.5) self.face_detection = self.mp_face_detection.FaceDetection( min_detection_confidence=0.5) self.results = [] self._bg_remover = None print("[Checker] Initializing with AI Face Parsing...") init_face_parser() print("[Checker] Ready with AI model") def _load_background_remover(self): if self._bg_remover is None: try: print("[AI] Loading background removal model...") try: from transformers import pipeline print("[AI] Trying U2Net model...") self._bg_remover = pipeline( "image-segmentation", model="briaai/RMBG-2.0", # مدل جدیدتر device=-1 # CPU ) print("[AI] ✓ Background model loaded (RMBG-2.0)") return self._bg_remover except Exception as e1: print(f"[AI] RMBG-2.0 failed: {e1}") try: print("[AI] Trying DeepLabV3...") from transformers import AutoImageProcessor, AutoModelForSemanticSegmentation import torch processor = AutoImageProcessor.from_pretrained( "nvidia/segformer-b0-finetuned-ade-512-512" ) model = AutoModelForSemanticSegmentation.from_pretrained( "nvidia/segformer-b0-finetuned-ade-512-512" ) self._bg_remover = { 'processor': processor, 'model': model, 'type': 'segformer' } print("[AI] ✓ Background model loaded (SegFormer)") return self._bg_remover except Exception as e2: print(f"[AI] SegFormer failed: {e2}") print("[AI] ⚠ Using CV2 fallback for background") self._bg_remover = False except Exception as e: print(f"[AI] Background model initialization failed: {e}") self._bg_remover = False return self._bg_remover def add_result(self, category, requirement, status, message, details=""): self.results.append({ 'category': category, 'requirement': requirement, 'status': status, 'message': message, 'details': details }) def _get_background_mask(self, image, landmarks, img_width, img_height): bg_remover = self._load_background_remover() if bg_remover and bg_remover is not False: try: pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) if callable(bg_remover): result = bg_remover(pil_image) if isinstance(result, list) and len(result) > 0: for item in result: if 'label' in item and ('person' in item['label'].lower() or 'human' in item['label'].lower()): mask_pil = item['mask'] mask = np.array(mask_pil) if len(mask.shape) == 3: mask = cv2.cvtColor(mask, cv2.COLOR_RGB2GRAY) _, binary_mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY) if binary_mask.shape[:2] != (img_height, img_width): binary_mask = cv2.resize(binary_mask, (img_width, img_height)) bg_mask = cv2.bitwise_not(binary_mask) print("[AI] ✓ Background mask extracted") return bg_mask elif isinstance(bg_remover, dict) and bg_remover.get('type') == 'segformer': import torch processor = bg_remover['processor'] model = bg_remover['model'] inputs = processor(images=pil_image, return_tensors="pt") with torch.no_grad(): outputs = model(**inputs) logits = outputs.logits upsampled = torch.nn.functional.interpolate( logits, size=(img_height, img_width), mode="bilinear", align_corners=False ) seg_mask = upsampled.argmax(dim=1)[0].cpu().numpy() person_mask = (seg_mask == 12).astype(np.uint8) * 255 bg_mask = cv2.bitwise_not(person_mask) print("[AI] ✓ Background mask extracted (SegFormer)") return bg_mask except Exception as e: print(f"[AI] Background segmentation failed: {e}") import traceback traceback.print_exc() print("[AI] Using CV2 fallback for background mask") return self._get_background_mask_cv(image, landmarks, img_width, img_height) def _get_background_mask_cv(self, image, landmarks, img_width, img_height): h, w = image.shape[:2] face_outline = [10, 338, 297, 332, 284, 251, 389, 356, 454, 323, 361, 288, 397, 365, 379, 378, 400, 377, 152, 148, 176, 149, 150, 136, 172, 58, 132, 93, 234, 127, 162] face_points = [] for idx in face_outline: x = int(landmarks.landmark[idx].x * img_width) y = int(landmarks.landmark[idx].y * img_height) face_points.append([x, y]) face_mask = np.zeros((h, w), dtype=np.uint8) cv2.fillPoly(face_mask, [np.array(face_points)], 255) kernel = np.ones((50, 50), np.uint8) face_mask = cv2.dilate(face_mask, kernel, iterations=1) return cv2.bitwise_not(face_mask) def check_eyeglasses_parsing(self, parsing_mask, image): try: h, w = image.shape[:2] mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) glasses_pixels = np.sum(mask_resized == 3) # ← تغییر از 6 به 3 glasses_ratio = glasses_pixels / (h * w) if glasses_ratio > 0.005: self.add_result("Facial Features", "Eyeglasses", "fail", f"Eyeglasses detected (AI: {min(glasses_ratio*100, 100):.1f}%)", "Eyeglasses not allowed. Exception: Medical reasons with doctor's statement.") elif glasses_ratio > 0.002: self.add_result("Facial Features", "Eyeglasses", "warning", "Possible eyeglasses detected", "If wearing glasses, remove and retake.") else: self.add_result("Facial Features", "Eyeglasses", "pass", "No eyeglasses detected (AI)", "Meets requirement") except Exception as e: print(f"[Parsing] Eyeglasses error: {e}") self.check_eyeglasses_cv_fallback(image, None, w, h) def check_headwear_parsing(self, parsing_mask, image): try: h, w = image.shape[:2] mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) hat_pixels = np.sum(mask_resized == 14) # ← تغییر از 18 به 14 hat_ratio = hat_pixels / (h * w) if hat_pixels > 0: hat_y_coords, hat_x_coords = np.where(mask_resized == 14) if len(hat_y_coords) > 0: avg_hat_y = np.mean(hat_y_coords) if avg_hat_y > h * 0.3: print(f"[DEBUG] Hat pixels in wrong location (y={avg_hat_y:.0f}/{h}), ignoring") hat_pixels = 0 hat_ratio = 0 print(f"[DEBUG] Hat: {hat_pixels} pixels ({hat_ratio*100:.4f}%), location check passed") MIN_HAT_PIXELS = 2000 FAIL_RATIO = 0.035 WARN_RATIO = 0.018 if hat_pixels < MIN_HAT_PIXELS: self.add_result("Head Covering", "Headwear/Hat", "pass", f"No headwear (AI: {hat_pixels} pixels - likely noise)", "Meets requirement") elif hat_ratio > FAIL_RATIO: head_region = mask_resized[:int(h*0.3), :] hat_in_head = np.sum(head_region == 14) coverage = (hat_in_head / head_region.size) * 100 self.add_result("Head Covering", "Headwear/Hat", "fail", f"Headwear detected (AI: {hat_pixels} pixels, {coverage:.1f}% head coverage)", "Do not wear hats. Exception: Religious covering worn daily.") elif hat_ratio > WARN_RATIO: self.add_result("Head Covering", "Headwear/Hat", "warning", f"Possible headwear/large hair accessory (AI: {hat_pixels} pixels)", "If wearing hat/large accessory, remove. If hair/hijab, proceed.") else: self.add_result("Head Covering", "Headwear/Hat", "pass", f"No significant headwear (AI: {hat_pixels} pixels)", "Meets requirement") except Exception as e: print(f"[Parsing] Headwear error: {e}") import traceback traceback.print_exc() self.check_headwear_cv_fallback(image, None, w, h) def check_eyes_open_parsing(self, parsing_mask, landmarks, img_width, img_height, image): try: h, w = image.shape[:2] mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) left_eye_pixels = np.sum(mask_resized == 4) right_eye_pixels = np.sum(mask_resized == 5) total_eye_pixels = left_eye_pixels + right_eye_pixels eye_ratio = total_eye_pixels / (h * w) print(f"[DEBUG] Eyes: L={left_eye_pixels}, R={right_eye_pixels}, ratio={eye_ratio*100:.4f}%") if eye_ratio > 0.0005: self.add_result("Facial Expression", "Eyes Open", "pass", f"Both eyes clearly open (AI: {total_eye_pixels} pixels)", "Neutral expression met") elif eye_ratio > 0.0008: self.add_result("Facial Expression", "Eyes Open", "warning", f"Eyes may be partially closed (AI: {total_eye_pixels} pixels)", "Both eyes must be fully open") else: glasses_pixels = np.sum(mask_resized == 6) if glasses_pixels > total_eye_pixels * 5: self.add_result("Facial Expression", "Eyes Open", "warning", "Eyes obscured by eyeglasses", "Eyes must be visible") else: self.add_result("Facial Expression", "Eyes Open", "fail", f"Eyes appear closed (AI: {total_eye_pixels} pixels)", "Both eyes must be fully open") except Exception as e: print(f"[Parsing] Eyes error: {e}") self.check_eyes_open_cv(landmarks, img_height) def check_jewelry_parsing(self, parsing_mask, image): try: h, w = image.shape[:2] mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) earring_ratio = np.sum(mask_resized == 15) / (h * w) # ← تغییر از 9 به 15 necklace_ratio = np.sum(mask_resized == 16) / (h * w) # ← تغییر از 15 به 16 jewelry_detected = [] if earring_ratio > 0.001: jewelry_detected.append("earrings") if necklace_ratio > 0.002: jewelry_detected.append("necklace") if jewelry_detected: jewelry_str = " and ".join(jewelry_detected) confidence = max(earring_ratio, necklace_ratio) * 100 self.add_result("Accessories", "Jewelry", "warning", f"{jewelry_str.capitalize()} detected (AI: {confidence:.1f}%)", "Visible jewelry should be minimal.") else: self.add_result("Accessories", "Jewelry", "pass", "No prominent jewelry detected (AI)", "Meets requirement") except Exception as e: print(f"[Parsing] Jewelry error: {e}") self.add_result("Accessories", "Jewelry", "pass", "Unable to verify", "Manual review recommended") def check_face_covering_parsing(self, parsing_mask, landmarks, img_width, img_height, image): try: h, w = image.shape[:2] mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) face_indices = [234, 127, 162, 21, 54, 103, 67, 109, 10, 338, 297, 332, 284, 251, 389, 356, 454, 152, 148, 176, 149, 150, 136, 172, 58, 132, 93] face_points = [[int(landmarks.landmark[i].x * img_width), int(landmarks.landmark[i].y * img_height)] for i in face_indices] face_mask = np.zeros((h, w), dtype=np.uint8) cv2.fillPoly(face_mask, [np.array(face_points)], 255) face_region = mask_resized[face_mask > 0] if len(face_region) == 0: return skin_ratio = np.sum(face_region == 1) / len(face_region) hair_ratio = np.sum(face_region == 13) / len(face_region) # ← تغییر از 17 به 13 print(f"[DEBUG] Face Covering: skin={skin_ratio*100:.1f}%, hair={hair_ratio*100:.1f}%") if skin_ratio < 0.30: self.add_result("Head Covering", "Face Covering", "fail", f"Face significantly covered (AI: {skin_ratio*100:.0f}% visible)", "Full face must be visible") elif hair_ratio > 0.25: self.add_result("Head Covering", "Face Covering", "warning", f"Hair may be covering face ({hair_ratio*100:.0f}%)", "Ensure hair pulled back") else: self.add_result("Head Covering", "Face Covering", "pass", f"Face fully visible (AI: {skin_ratio*100:.0f}% skin)", "No obstruction") except Exception as e: print(f"[Parsing] Face covering error: {e}") self.add_result("Head Covering", "Face Covering", "pass", "Unable to verify", "Manual review recommended") def check_eyes_open_cv(self, landmarks, img_height): left_top = landmarks.landmark[159].y left_bottom = landmarks.landmark[145].y left_opening = abs(left_top - left_bottom) * img_height right_top = landmarks.landmark[386].y right_bottom = landmarks.landmark[374].y right_opening = abs(right_top - right_bottom) * img_height avg_opening = (left_opening + right_opening) / 2 if avg_opening > img_height * 0.012: self.add_result("Facial Expression", "Eyes Open", "pass", "Both eyes open (CV)", "Meets requirement") elif avg_opening > img_height * 0.008: self.add_result("Facial Expression", "Eyes Open", "warning", "Eyes may be partially closed", "Both eyes must be fully open") else: self.add_result("Facial Expression", "Eyes Open", "fail", "Eyes appear closed", "Both eyes must be fully open") def check_eyeglasses_cv_fallback(self, image, landmarks, img_width, img_height): self.add_result("Facial Features", "Eyeglasses", "pass", "Unable to verify with AI", "Manual review recommended") def check_headwear_cv_fallback(self, image, landmarks, img_width, img_height): self.add_result("Head Covering", "Headwear/Hat", "pass", "Unable to verify with AI", "Manual review recommended") def check_dimensions(self, image): h, w = image.shape[:2] if h == w and 600 <= w <= 1200: self.add_result("Dimensions", "Image Size", "pass", f"{w}×{h} pixels", "Meets requirements") else: self.add_result("Dimensions", "Image Size", "fail", f"{w}×{h} pixels", "Must be square (600-1200px)") def check_color_depth(self, image): if len(image.shape) == 3 and image.shape[2] == 3: b, g, r = cv2.split(image) avg_diff = (np.abs(b.astype(float) - g.astype(float)).mean() + np.abs(b.astype(float) - r.astype(float)).mean() + np.abs(g.astype(float) - r.astype(float)).mean()) / 3 if avg_diff < 5: self.add_result("Technical", "Color Depth", "warning", "Image appears grayscale", "Must be in color") return False else: self.add_result("Technical", "Color Depth", "pass", "Image in color (24-bit RGB)", "Meets requirement") return True else: self.add_result("Technical", "Color Depth", "fail", "Image is grayscale", "Must be in color") return False def check_face_detection(self, image): rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) face_results = self.face_mesh.process(rgb_image) if face_results.multi_face_landmarks: num_faces = len(face_results.multi_face_landmarks) if num_faces == 1: self.add_result("Composition", "Number of People", "pass", "Exactly one face detected", "Meets requirement") return face_results.multi_face_landmarks[0] else: self.add_result("Composition", "Number of People", "fail", f"{num_faces} faces detected", "Only one person allowed") else: self.add_result("Composition", "Face Detection", "fail", "No face detected", "Face must be visible") return None def check_face_angle(self, landmarks, img_width, img_height): nose_x = landmarks.landmark[4].x * img_width left_face = landmarks.landmark[234].x * img_width right_face = landmarks.landmark[454].x * img_width face_center = (left_face + right_face) / 2 deviation_percent = abs(nose_x - face_center) / img_width * 100 if deviation_percent < 2: self.add_result("Head Position", "Face Direction", "pass", "Face directly facing camera", "Full-face view met") elif deviation_percent < 4: self.add_result("Head Position", "Face Direction", "warning", "Face slightly turned", "Must be in full-face view") else: self.add_result("Head Position", "Face Direction", "fail", "Face significantly turned", "Must be in full-face view") def check_red_eye(self, image, landmarks, img_width, img_height): def check_eye_redness(eye_indices): eye_points = [[int(landmarks.landmark[i].x * img_width), int(landmarks.landmark[i].y * img_height)] for i in eye_indices] mask = np.zeros((img_height, img_width), dtype=np.uint8) cv2.fillPoly(mask, [np.array(eye_points)], 255) pixels = image[mask > 0] if len(pixels) > 0: b, g, r = cv2.split(image) red_mean = r[mask > 0].mean() green_mean = g[mask > 0].mean() blue_mean = b[mask > 0].mean() if red_mean > green_mean * 1.4 and red_mean > blue_mean * 1.4 and red_mean > 100: return True return False left_eye_indices = [33, 133, 160, 159, 158, 157, 173] right_eye_indices = [362, 263, 387, 386, 385, 384, 398] if check_eye_redness(left_eye_indices) or check_eye_redness(right_eye_indices): self.add_result("Photo Quality", "Red Eye Effect", "fail", "Red eye effect detected", "Photo must not have red eye") else: self.add_result("Photo Quality", "Red Eye Effect", "pass", "No red eye effect detected", "Meets requirement") def check_image_quality(self, image): gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var() if laplacian_var > 150: self.add_result("Photo Quality", "Sharpness", "pass", "Image sharp and in focus", "Meets requirement") elif laplacian_var > 80: self.add_result("Photo Quality", "Sharpness", "warning", "Image slightly soft", "Should be sharp") else: self.add_result("Photo Quality", "Sharpness", "fail", "Image is blurry", "Must be sharp") blur = cv2.GaussianBlur(gray, (5, 5), 0) noise = cv2.subtract(gray, blur) noise_level = np.std(noise) if noise_level < 8: self.add_result("Photo Quality", "Grain/Noise", "pass", "Minimal grain/noise", "Meets requirement") elif noise_level < 15: self.add_result("Photo Quality", "Grain/Noise", "warning", "Noticeable grain/noise", "Use better camera") else: self.add_result("Photo Quality", "Grain/Noise", "fail", "Image is grainy/noisy", "Use better camera") def check_head_proportions(self, landmarks, img_height, img_width): h, w = img_height, img_width eye_indices = [33, 133, 362, 263] eye_y = np.mean([landmarks.landmark[i].y for i in eye_indices]) * h eye_from_bottom = 100 - (eye_y / h) * 100 if hasattr(self, '_current_parsing_mask') and self._current_parsing_mask is not None: parsing_mask = self._current_parsing_mask mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) hair_pixels = np.sum(mask_resized == 13) hat_pixels = np.sum(mask_resized == 14) total_head_coverage = hair_pixels + hat_pixels head_region = mask_resized[:int(h*0.4), :] # 40% بالایی تصویر head_coverage_ratio = total_head_coverage / (head_region.size) is_head_covered = hair_pixels < 3000 or head_coverage_ratio < 0.15 print(f"[DEBUG] Head Coverage Analysis:") print(f" Hair pixels: {hair_pixels}") print(f" Hat/covering pixels: {hat_pixels}") print(f" Coverage ratio: {head_coverage_ratio*100:.2f}%") print(f" Is head covered: {is_head_covered}") top_of_head = h # مقدار اولیه if is_head_covered: print("[HEAD] Covered head detected - using hybrid method") if hat_pixels > 1000: hat_y_coords, _ = np.where(mask_resized == 14) if len(hat_y_coords) > 0: top_of_head = np.min(hat_y_coords) print(f"[HEAD] Using hat/covering top: {top_of_head}") elif hair_pixels > 500: hair_y_coords, _ = np.where(mask_resized == 13) if len(hair_y_coords) > 0: top_of_head = np.min(hair_y_coords) print(f"[HEAD] Using minimal hair top: {top_of_head}") else: head_skin_region = mask_resized[:int(h*0.3), :] skin_y_coords, _ = np.where(head_skin_region == 1) if len(skin_y_coords) > 0: top_of_head = np.min(skin_y_coords) print(f"[HEAD] Using head skin top: {top_of_head}") if top_of_head >= h * 0.5: # اگر خیلی پایین بود print("[HEAD] Segmentation insufficient, using geometric estimation") forehead_indices = [10, 338, 297, 332, 284, 251, 389, 356, 454] min_forehead_y = min([landmarks.landmark[i].y for i in forehead_indices]) * h eye_to_forehead = min_forehead_y - eye_y if hat_pixels > 1000: hair_extension = eye_to_forehead * 0.85 else: hair_extension = eye_to_forehead * 0.70 top_of_head = max(0, min_forehead_y - hair_extension) print(f"[HEAD] Estimated top using forehead + {hair_extension:.0f}px extension: {top_of_head}") else: print("[HEAD] Open head detected - using standard method") hair_y_coords, _ = np.where(mask_resized == 13) if len(hair_y_coords) > 0: top_of_head = np.min(hair_y_coords) print(f"[HEAD] Using hair top: {top_of_head}") else: head_region = mask_resized[:int(h*0.3), :] skin_y_coords, _ = np.where(head_region == 1) if len(skin_y_coords) > 0: top_of_head = np.min(skin_y_coords) print(f"[HEAD] Using head skin: {top_of_head}") else: forehead_indices = [10, 338, 297, 332, 284, 251, 389, 356, 454] min_forehead_y = min([landmarks.landmark[i].y for i in forehead_indices]) * h eye_to_forehead = min_forehead_y - eye_y hair_extension = eye_to_forehead * 0.65 top_of_head = max(0, min_forehead_y - hair_extension) print(f"[HEAD] Fallback to landmarks: {top_of_head}") chin_y = 0 lower_face_region = mask_resized[int(h*0.5):, :] skin_y_coords, _ = np.where(lower_face_region == 1) if len(skin_y_coords) > 0: chin_y = np.max(skin_y_coords) + int(h*0.5) else: chin_y = landmarks.landmark[152].y * h face_height_pixels = chin_y - top_of_head face_height_ratio = (face_height_pixels / h) * 100 if face_height_ratio < 35 or face_height_ratio > 85: print(f"[HEAD] WARNING: Unrealistic face height {face_height_ratio:.1f}%, using fallback") chin_y = landmarks.landmark[152].y * h forehead_indices = [10, 338, 297, 332, 284, 251, 389, 356, 454] min_forehead_y = min([landmarks.landmark[i].y for i in forehead_indices]) * h eye_to_forehead = min_forehead_y - eye_y hair_extension = eye_to_forehead * (0.85 if is_head_covered else 0.65) top_of_head = max(0, min_forehead_y - hair_extension) face_height_pixels = chin_y - top_of_head face_height_ratio = (face_height_pixels / h) * 100 print(f"[HEAD] Corrected face height: {face_height_ratio:.1f}%") if not hasattr(self, '_debug_data'): self._debug_data = {} self._debug_data.update({ 'top_of_head': top_of_head, 'chin_y': chin_y, 'eye_y': eye_y, 'face_height_pixels': face_height_pixels, 'face_height_ratio': face_height_ratio, 'is_head_covered': is_head_covered, 'hair_pixels': hair_pixels, 'hat_pixels': hat_pixels, 'method': 'AI_Segmentation_Enhanced' }) else: chin_y = landmarks.landmark[152].y * h forehead_indices = [10, 338, 297, 332, 284, 251, 389, 356, 454] min_forehead_y = min([landmarks.landmark[i].y for i in forehead_indices]) * h eye_to_forehead = min_forehead_y - eye_y hair_extension = eye_to_forehead * 0.75 # مقدار متوسط top_of_head = max(0, min_forehead_y - hair_extension) face_height_pixels = chin_y - top_of_head face_height_ratio = (face_height_pixels / h) * 100 if not hasattr(self, '_debug_data'): self._debug_data = {} self._debug_data.update({ 'method': 'MediaPipe_Fallback' }) if 56 <= eye_from_bottom <= 69: self.add_result("Head Position", "Eye Height", "pass", f"Eye height: {eye_from_bottom:.1f}% from bottom", "Meets requirement (56-69%)") else: issue = "Eyes too low" if eye_from_bottom < 56 else "Eyes too high" suggestion = "Move camera down" if eye_from_bottom < 56 else "Move camera up" self.add_result("Head Position", "Eye Height", "fail", f"Eye height: {eye_from_bottom:.1f}%. {issue}", f"Eyes must be 56-69% from bottom. {suggestion}") if 50 <= face_height_ratio <= 69: self.add_result("Head Position", "Face Height", "pass", f"Face height: {face_height_ratio:.1f}% (top to chin)", "Meets requirement (50-69%)") else: issue = "Head too small" if face_height_ratio < 50 else "Head too large" suggestion = "Move closer" if face_height_ratio < 50 else "Move further" self.add_result("Head Position", "Face Height", "fail", f"Face height: {face_height_ratio:.1f}%. {issue}", f"Head must be 50-69% of image. {suggestion}") def check_background_ai(self, image, landmarks, bg_mask, img_width, img_height): try: h, w = image.shape[:2] if hasattr(self, '_current_parsing_mask') and self._current_parsing_mask is not None: parsing_mask = self._current_parsing_mask mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) bg_mask_from_parsing = (mask_resized == 0).astype(np.uint8) * 255 if np.sum(bg_mask_from_parsing > 0) > (h * w * 0.1): print("[BG] Using parsing mask for background detection") bg_mask = bg_mask_from_parsing else: print("[BG] Parsing mask insufficient, using CV fallback") bg_pixels = image[bg_mask > 0] if len(bg_pixels) == 0: self.add_result("Background", "Color", "fail", "Cannot analyze background", "Background not detected") return bg_mean = np.mean(bg_pixels.reshape(-1, 3), axis=0) brightness = np.mean(bg_mean) color_variance = np.std(bg_mean) is_neutral = color_variance < 15 if brightness > 200 and is_neutral: self.add_result("Background", "Color", "pass", f"Background white/off-white (brightness: {brightness:.0f})", "Plain white requirement met") elif brightness > 180 and is_neutral: self.add_result("Background", "Color", "warning", f"Background light (brightness: {brightness:.0f})", "Should be plain white") else: self.add_result("Background", "Color", "fail", f"Background not white (brightness: {brightness:.0f})", "Must be plain white or off-white") gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) bg_gray = cv2.bitwise_and(gray, gray, mask=bg_mask) bg_gray_pixels = bg_gray[bg_mask > 0] bg_std = np.std(bg_gray_pixels) print(f"[BG-Uniformity] STD: {bg_std:.2f}, Brightness: {brightness:.1f}") edges = cv2.Canny(bg_gray, 30, 100) edge_pixels = np.sum(edges > 0) edge_ratio = edge_pixels / np.sum(bg_mask > 0) if brightness > 200: std_threshold_high = 12 std_threshold_low = 8 edge_threshold_high = 0.02 edge_threshold_low = 0.01 elif brightness > 180: std_threshold_high = 15 std_threshold_low = 10 edge_threshold_high = 0.025 edge_threshold_low = 0.015 else: std_threshold_high = 20 std_threshold_low = 12 edge_threshold_high = 0.03 edge_threshold_low = 0.02 has_texture = False texture_level = "none" if bg_std > std_threshold_high or edge_ratio > edge_threshold_high: has_texture = True texture_level = "high" elif bg_std > std_threshold_low or edge_ratio > edge_threshold_low: has_texture = True texture_level = "slight" print(f"[BG-Uniformity] Edge ratio: {edge_ratio:.4f}, Texture: {texture_level}") if not has_texture: self.add_result("Background", "Uniformity", "pass", "Background plain and uniform", "No patterns detected") elif texture_level == "slight": if brightness > 190 and bg_std < std_threshold_low * 1.5: self.add_result("Background", "Uniformity", "pass", "Background uniform with minimal natural variation", "Slight natural shadow acceptable") else: self.add_result("Background", "Uniformity", "warning", "Background has slight texture", "Should be completely plain") else: self.add_result("Background", "Uniformity", "fail", "Background has visible patterns", "Must be plain") except Exception as e: print(f"[AI] Background check error: {e}") import traceback traceback.print_exc() self.add_result("Background", "Analysis", "pass", "Unable to verify", "Manual review recommended") def detect_shadow_mask(self, image, parsing_mask=None): try: h, w = image.shape[:2] if parsing_mask is not None: mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) bg_mask = (mask_resized == 0).astype(np.uint8) * 255 print("[Shadow] Using parsing mask for background") else: print("[Shadow] Parsing mask not available") return None, None if np.sum(bg_mask > 0) < (h * w * 0.05): print("[Shadow] Background area too small") return None, None lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB) l_channel = lab[:, :, 0] bg_l = cv2.bitwise_and(l_channel, l_channel, mask=bg_mask) bg_pixels = l_channel[bg_mask > 0] if len(bg_pixels) == 0: return None, None bg_mean = np.mean(bg_pixels) bg_std = np.std(bg_pixels) bg_median = np.median(bg_pixels) print(f"[Shadow] BG Stats - Mean: {bg_mean:.1f}, Median: {bg_median:.1f}, STD: {bg_std:.1f}") shadow_threshold_1 = bg_mean - bg_std * 1.2 shadow_threshold_2 = bg_median - bg_std * 1.0 shadow_threshold = min(shadow_threshold_1, shadow_threshold_2) if bg_mean > 200: shadow_threshold = bg_mean - max(bg_std * 1.5, 25) print(f"[Shadow] Threshold: {shadow_threshold:.1f}") shadow_mask_raw = np.zeros((h, w), dtype=np.uint8) shadow_mask_raw[bg_mask > 0] = ((l_channel[bg_mask > 0] < shadow_threshold) * 255).astype(np.uint8) kernel_small = np.ones((3, 3), np.uint8) kernel_medium = np.ones((5, 5), np.uint8) shadow_mask_clean = cv2.morphologyEx(shadow_mask_raw, cv2.MORPH_OPEN, kernel_small) shadow_mask_clean = cv2.morphologyEx(shadow_mask_clean, cv2.MORPH_CLOSE, kernel_medium) shadow_mask_clean = cv2.GaussianBlur(shadow_mask_clean, (5, 5), 0) _, shadow_mask_clean = cv2.threshold(shadow_mask_clean, 127, 255, cv2.THRESH_BINARY) shadow_pixels = np.sum(shadow_mask_clean > 0) bg_pixels_count = np.sum(bg_mask > 0) shadow_ratio = shadow_pixels / bg_pixels_count if bg_pixels_count > 0 else 0 if shadow_pixels > 0: shadow_values = l_channel[shadow_mask_clean > 0] shadow_mean = np.mean(shadow_values) contrast = bg_mean - shadow_mean else: shadow_mean = 0 contrast = 0 shadow_info = { 'shadow_ratio': shadow_ratio, 'shadow_pixels': shadow_pixels, 'bg_mean': bg_mean, 'shadow_mean': shadow_mean, 'contrast': contrast, 'bg_std': bg_std } print(f"[Shadow] Detected: {shadow_ratio*100:.1f}% of background, Contrast: {contrast:.1f}") return shadow_mask_clean, shadow_info except Exception as e: print(f"[Shadow] Detection error: {e}") import traceback traceback.print_exc() return None, None def create_shadow_visualization(self, image, shadow_mask, parsing_mask=None): try: h, w = image.shape[:2] overlay = image.copy() shadow_color = np.array([0, 0, 255], dtype=np.uint8) overlay[shadow_mask > 0] = cv2.addWeighted( overlay[shadow_mask > 0], 0.4, np.full_like(overlay[shadow_mask > 0], shadow_color), 0.6, 0 ) contours, _ = cv2.findContours(shadow_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(overlay, contours, -1, (0, 0, 255), 2) legend_x = 20 legend_y = h - 60 overlay_bg = overlay.copy() cv2.rectangle(overlay_bg, (legend_x - 10, legend_y - 10), (legend_x + 200, legend_y + 35), (0, 0, 0), -1) cv2.addWeighted(overlay_bg, 0.7, overlay, 0.3, 0, overlay) cv2.putText(overlay, "Shadow Area", (legend_x + 35, legend_y + 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2) cv2.rectangle(overlay, (legend_x, legend_y - 5), (legend_x + 25, legend_y + 15), (0, 0, 255), -1) cv2.rectangle(overlay, (legend_x, legend_y - 5), (legend_x + 25, legend_y + 15), (255, 255, 255), 1) shadow_ratio = np.sum(shadow_mask > 0) / (h * w) text = f"Shadow: {shadow_ratio*100:.1f}%" cv2.putText(overlay, text, (legend_x, legend_y - 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2) return overlay except Exception as e: print(f"[Shadow] Visualization error: {e}") return image def check_shadows_ai(self, image, landmarks, bg_mask, img_width, img_height): try: h, w = image.shape[:2] gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) face_indices = [234, 127, 162, 21, 54, 103, 67, 109, 10, 338, 297, 332] face_points = [[int(landmarks.landmark[i].x * img_width), int(landmarks.landmark[i].y * img_height)] for i in face_indices] face_mask = np.zeros((h, w), dtype=np.uint8) cv2.fillPoly(face_mask, [np.array(face_points)], 255) face_pixels = gray[face_mask > 0] if len(face_pixels) > 0: face_mean = np.mean(face_pixels) face_std = np.std(face_pixels) dark_ratio = np.sum(face_pixels < face_mean - face_std * 1.5) / len(face_pixels) if dark_ratio < 0.15: self.add_result("Lighting", "Face Shadows", "pass", "No significant shadows on face", "Even lighting met") elif dark_ratio < 0.25: self.add_result("Lighting", "Face Shadows", "warning", "Slight shadows on face", "Lighting should be even") else: self.add_result("Lighting", "Face Shadows", "fail", "Shadows detected on face", "Must have even lighting") parsing_mask = None if hasattr(self, '_current_parsing_mask'): parsing_mask = self._current_parsing_mask shadow_mask, shadow_info = self.detect_shadow_mask(image, parsing_mask) if shadow_mask is not None and shadow_info is not None: self._shadow_mask = shadow_mask self._shadow_info = shadow_info shadow_ratio = shadow_info['shadow_ratio'] contrast = shadow_info['contrast'] if shadow_ratio < 0.05: self.add_result("Lighting", "Background Shadows", "pass", f"Minimal background shadow ({shadow_ratio*100:.1f}%, contrast: {contrast:.0f})", "Natural shadow acceptable") elif shadow_ratio < 0.15: if contrast < 40: self.add_result("Lighting", "Background Shadows", "pass", f"Slight shadow with low contrast ({shadow_ratio*100:.1f}%, contrast: {contrast:.0f})", "Acceptable shadow level") else: self.add_result("Lighting", "Background Shadows", "warning", f"Moderate shadow detected ({shadow_ratio*100:.1f}%, contrast: {contrast:.0f})", "Consider repositioning lighting or moving away from wall") else: self.add_result("Lighting", "Background Shadows", "fail", f"Strong shadow cast on background ({shadow_ratio*100:.1f}%, contrast: {contrast:.0f})", "Move away from background or improve lighting setup") else: print("[Shadow] Using fallback method") bg_gray_pixels = gray[bg_mask > 0] if len(bg_gray_pixels) > 0: bg_mean = np.mean(bg_gray_pixels) bg_std = np.std(bg_gray_pixels) if bg_std < 15: self.add_result("Lighting", "Background Shadows", "pass", f"Background uniform (STD: {bg_std:.1f})", "No significant shadows") elif bg_std < 25: self.add_result("Lighting", "Background Shadows", "warning", "Slight variation in background", "May indicate shadow") else: self.add_result("Lighting", "Background Shadows", "fail", "Non-uniform background detected", "Check for shadows") except Exception as e: print(f"[AI] Shadow check error: {e}") import traceback traceback.print_exc() self.add_result("Lighting", "Shadows", "pass", "Unable to verify", "Manual review recommended") def check_image_from_base64(self, image_base64): try: self.results = [] self._current_parsing_mask = None self._debug_data = {} image_bytes = base64.b64decode(image_base64) nparr = np.frombuffer(image_bytes, np.uint8) image = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if image is None: raise ValueError("Failed to decode") h, w = image.shape[:2] image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) image_pil = Image.fromarray(image_rgb) parsing_mask = predict_face_parsing(image_pil) if parsing_mask is not None: self._current_parsing_mask = parsing_mask print(f"[Checker] ✓ Parsing mask saved") self.check_dimensions(image) self.check_color_depth(image) face_landmarks = self.check_face_detection(image) if face_landmarks: bg_mask = self._get_background_mask(image, face_landmarks, w, h) self.check_head_proportions(face_landmarks, h, w) self.check_face_angle(face_landmarks, w, h) if parsing_mask is not None: self.check_eyeglasses_parsing(parsing_mask, image) self.check_headwear_parsing(parsing_mask, image) self.check_eyes_open_parsing(parsing_mask, face_landmarks, w, h, image) self.check_jewelry_parsing(parsing_mask, image) self.check_face_covering_parsing(parsing_mask, face_landmarks, w, h, image) else: print("[Checker] Parsing unavailable, using fallback") self.check_eyes_open_cv(face_landmarks, h) self.check_eyeglasses_cv_fallback(image, face_landmarks, w, h) self.check_headwear_cv_fallback(image, face_landmarks, w, h) self.check_background_ai(image, face_landmarks, bg_mask, w, h) self.check_shadows_ai(image, face_landmarks, bg_mask, w, h) self.check_red_eye(image, face_landmarks, w, h) self.check_image_quality(image) html_report = self.generate_html_report() parsing_colored_b64 = None parsing_overlay_b64 = None debug_face_height_b64 = None shadow_viz_b64 = None if parsing_mask is not None: unique_labels = np.unique(parsing_mask).tolist() colored = create_colored_mask(parsing_mask) colored_legend = add_legend_to_image(colored, unique_labels) _, buf1 = cv2.imencode('.jpg', cv2.cvtColor(colored_legend, cv2.COLOR_RGB2BGR)) parsing_colored_b64 = base64.b64encode(buf1).decode('utf-8') overlay = create_transparent_overlay(image_pil, parsing_mask, alpha=0.4) overlay_legend = add_legend_to_image(overlay, unique_labels) _, buf2 = cv2.imencode('.jpg', cv2.cvtColor(overlay_legend, cv2.COLOR_RGB2BGR)) parsing_overlay_b64 = base64.b64encode(buf2).decode('utf-8') print("[Checker] ✓ Visualizations created") if face_landmarks and hasattr(self, '_debug_data') and self._debug_data: try: eye_indices = [33, 133, 362, 263] eye_y = np.mean([face_landmarks.landmark[i].y for i in eye_indices]) * h mask_resized = cv2.resize(parsing_mask.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) chin_region = mask_resized[int(h*0.4):, :] skin_pixels_y, _ = np.where(chin_region == 1) chin_y = (np.max(skin_pixels_y) + int(h*0.4)) if len(skin_pixels_y) > 0 else face_landmarks.landmark[152].y * h hair_pixels = np.sum(mask_resized == 13) if hair_pixels > 500: hair_y_coords, _ = np.where(mask_resized == 13) top_of_head = np.min(hair_y_coords) if len(hair_y_coords) > 0 else eye_y - 200 else: head_region = mask_resized[:int(h*0.5), :] skin_head_y, _ = np.where(head_region == 1) if len(skin_head_y) > 0: top_of_head = np.min(skin_head_y) else: forehead_indices = [10, 338, 297, 332, 284, 251, 389, 356, 454] min_forehead_y = min([face_landmarks.landmark[i].y for i in forehead_indices]) * h top_of_head = max(0, min_forehead_y - (min_forehead_y - eye_y) * 0.65) debug_img = self._create_face_height_debug_image( image, parsing_mask, top_of_head, chin_y, eye_y, h, w ) _, buf_debug = cv2.imencode('.jpg', debug_img, [cv2.IMWRITE_JPEG_QUALITY, 95]) debug_face_height_b64 = base64.b64encode(buf_debug).decode('utf-8') print("[Checker] ✓ Debug image created") except Exception as e: print(f"[Checker] Debug image failed: {e}") if hasattr(self, '_shadow_mask') and self._shadow_mask is not None: try: shadow_viz = self.create_shadow_visualization( image, self._shadow_mask, parsing_mask ) _, buf_shadow = cv2.imencode('.jpg', shadow_viz, [cv2.IMWRITE_JPEG_QUALITY, 95]) shadow_viz_b64 = base64.b64encode(buf_shadow).decode('utf-8') print("[Checker] ✓ Shadow visualization created") except Exception as e: print(f"[Checker] Shadow visualization failed: {e}") self._current_parsing_mask = None self._debug_data = {} if hasattr(self, '_shadow_mask'): delattr(self, '_shadow_mask') if hasattr(self, '_shadow_info'): delattr(self, '_shadow_info') return { 'service_type': 'checking', 'html_report': html_report, 'results': self.results, 'parsing_colored_mask': parsing_colored_b64, 'parsing_transparent_overlay': parsing_overlay_b64, 'debug_face_height': debug_face_height_b64, 'shadow_visualization': shadow_viz_b64 # ✅ اضافه کردن این خط } except Exception as e: import traceback traceback.print_exc() raise Exception(f"Checking error: {str(e)}") def _create_face_height_debug_image(self, image, parsing_mask, top_of_head, chin_y, eye_y, img_h, img_w): h, w = img_h, img_w debug_img = image.copy() RED, GREEN, BLUE = (0,0,255), (0,255,0), (255,0,0) YELLOW, WHITE, BLACK = (0,255,255), (255,255,255), (0,0,0) top_of_head, chin_y, eye_y = int(top_of_head), int(chin_y), int(eye_y) cv2.line(debug_img, (0, top_of_head), (w, top_of_head), RED, 3) cv2.line(debug_img, (0, chin_y), (w, chin_y), GREEN, 3) cv2.line(debug_img, (0, eye_y), (w, eye_y), BLUE, 2) measurement_x = int(w * 0.1) cv2.line(debug_img, (measurement_x, top_of_head), (measurement_x, chin_y), YELLOW, 4) cv2.arrowedLine(debug_img, (measurement_x, top_of_head+20), (measurement_x, top_of_head), YELLOW, 3, tipLength=0.3) cv2.arrowedLine(debug_img, (measurement_x, chin_y-20), (measurement_x, chin_y), YELLOW, 3, tipLength=0.3) face_height_pixels = chin_y - top_of_head face_height_ratio = (face_height_pixels / h) * 100 eye_from_bottom = 100 - (eye_y / h) * 100 face_status = "PASS ✓" if 50 <= face_height_ratio <= 69 else "FAIL ✗" eye_status = "PASS ✓" if 56 <= eye_from_bottom <= 69 else "FAIL ✗" annotations = [ (f"IMAGE: {h}px", 30, WHITE, 1.5), (f"TOP: {top_of_head}px", 60, RED, 1.5), (f"CHIN: {chin_y}px", 85, GREEN, 1.5), (f"EYE: {eye_y}px", 110, BLUE, 1.5), (f"", 130, WHITE, 0.5), (f"FACE: {face_height_pixels}px", 150, YELLOW, 1.5), (f"FACE %: {face_height_ratio:.1f}% {face_status}", 180, YELLOW, 2), (f"Required: 50-69%", 205, YELLOW, 1.5), (f"", 225, WHITE, 0.5), (f"EYE: {eye_from_bottom:.1f}% {eye_status}", 245, BLUE, 2), (f"Required: 56-69%", 270, BLUE, 1.5), ] overlay = debug_img.copy() cv2.rectangle(overlay, (10, 10), (350, 290), BLACK, -1) cv2.addWeighted(overlay, 0.75, debug_img, 0.25, 0, debug_img) for text, y_pos, color, thickness in annotations: if text: cv2.putText(debug_img, text, (20, y_pos), cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, int(thickness)) legend_x, legend_y = w - 220, 30 overlay = debug_img.copy() cv2.rectangle(overlay, (legend_x-10, legend_y-10), (w-10, legend_y+120), BLACK, -1) cv2.addWeighted(overlay, 0.75, debug_img, 0.25, 0, debug_img) cv2.putText(debug_img, "Legend:", (legend_x, legend_y+20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, WHITE, 2) legends = [("Top", RED, 45), ("Chin", GREEN, 70), ("Eye", BLUE, 95), ("Height", YELLOW, 120)] for label, color, y_offset in legends: y_pos = legend_y + y_offset cv2.line(debug_img, (legend_x, y_pos), (legend_x+30, y_pos), color, 3) cv2.putText(debug_img, label, (legend_x+40, y_pos+5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, WHITE, 1) overall_status = "PASS" if (50 <= face_height_ratio <= 69 and 56 <= eye_from_bottom <= 69) else "REVIEW" status_color = GREEN if overall_status == "PASS" else RED overlay = debug_img.copy() cv2.rectangle(overlay, (w//2-150, h-60), (w//2+150, h-10), BLACK, -1) cv2.addWeighted(overlay, 0.75, debug_img, 0.25, 0, debug_img) cv2.putText(debug_img, f"Status: {overall_status}", (w//2-130, h-25), cv2.FONT_HERSHEY_SIMPLEX, 0.9, status_color, 2) return debug_img def generate_html_report(self): passed = sum(1 for r in self.results if r['status'] == 'pass') failed = sum(1 for r in self.results if r['status'] == 'fail') warnings = sum(1 for r in self.results if r['status'] == 'warning') if failed == 0 and warnings == 0: overall, color, subtitle = "✓ ALL CHECKS PASSED", "#28a745", "Your photo meets all requirements" elif failed == 0: overall, color, subtitle = "⚠ MINOR ISSUES", "#ffc107", "Photo acceptable but could be improved" else: overall, color, subtitle = "✗ REQUIREMENTS NOT MET", "#dc3545", "Please review issues and consider retaking" html = f'''

{overall}

{subtitle}

Passed: {passed} | Warnings: {warnings} | Failed: {failed}

✨ AI-Enhanced (Face Detection + Smart Background)

''' categories = {} for result in self.results: cat = result['category'] if cat not in categories: categories[cat] = [] categories[cat].append(result) for category, results in categories.items(): html += f'
' html += f'

{category}

' for result in results: if result['status'] == 'pass': icon, bg, border, text_color = "✓", "#d4edda", "#28a745", "#155724" elif result['status'] == 'warning': icon, bg, border, text_color = "⚠", "#fff3cd", "#ffc107", "#856404" else: icon, bg, border, text_color = "✗", "#f8d7da", "#dc3545", "#721c24" html += f'''

{icon} {result["requirement"]}

{result["message"]}

{result["details"]}

''' html += '
' # html += ''' #
#

📋 U.S. Visa Photo Requirements

# #
#
''' # Add official requirements reference html += '''

📋 Official U.S. Visa Photo Requirements

📸 Tips for Best Results:
• Use a white blanket or sheet as background if wall is not white
• Ensure even lighting with no shadows on face or background
• Stand 4-5 feet away from background to avoid shadows
• Use natural light or diffused indoor lighting
• Avoid grainy photos - use good quality printer if printing
• Do not use photos from driver's licenses or copied from other documents
• No selfies or full-length photos

For complete requirements and photo examples, visit:
U.S. Department of State – Photo Requirements

''' html += '' return html app = Flask(__name__) WORKER_ID = os.getenv('WORKER_ID', 'worker-1') ORCHESTRATOR_URL = os.getenv('ORCHESTRATOR_URL', '') WORKER_URL = os.getenv('WORKER_URL', '') current_status = 'idle' current_job = None total_processed = 0 status_lock = threading.Lock() print("\n" + "="*60) print("PASSPORT PHOTO WORKER - XENOVA VERSION") print("="*60) print("[Init] PassportPhotoProcessor...") processor = PassportPhotoProcessor() print("[Init] ✓ Processor ready") print("[Init] PhotoRequirementsChecker...") checker = PhotoRequirementsChecker() print("[Init] ✓ Checker ready") print("[Init] Initializing Xenova Face Parsing...") try: if init_face_parser(): print("[Init] ✓ Xenova model loaded!") else: print("[Init] ⚠ Will initialize on first use") except Exception as e: print(f"[Init] ⚠ Error: {e}") print("="*60) print("WORKER READY") print("="*60 + "\n") @app.route('/ai_status', methods=['GET']) def ai_status(): return jsonify({ 'face_parsing': { 'available': FACE_PARSING_AVAILABLE, 'model': 'Xenova/face-parsing' if FACE_PARSING_AVAILABLE else 'N/A' }, 'transformers_available': TRANSFORMERS_AVAILABLE, 'worker_id': WORKER_ID, 'status': current_status }) @app.route('/health', methods=['GET']) def health_check(): with status_lock: return jsonify({ 'status': current_status, 'worker_id': WORKER_ID, 'total_processed': total_processed, 'current_job': current_job, 'timestamp': datetime.now().isoformat() }) @app.route('/process', methods=['POST']) def process_job(): global current_status, current_job with status_lock: if current_status == 'busy': return jsonify({'error': 'Worker busy'}), 503 data = request.json if not data or 'unique_id' not in data: return jsonify({'error': 'Missing unique_id'}), 400 unique_id = data['unique_id'] service_type = data.get('service_type', 'processing') image_data = data.get('image_data') if not image_data: return jsonify({'error': 'Missing image_data'}), 400 with status_lock: current_status = 'busy' current_job = unique_id thread = threading.Thread(target=process_job_async, args=(unique_id, service_type, image_data)) thread.daemon = True thread.start() return jsonify({'message': 'Job accepted', 'worker_id': WORKER_ID}), 200 def process_job_async(unique_id, service_type, image_data): global current_status, current_job, total_processed try: print(f"[{WORKER_ID}] Processing job {unique_id} - Type: {service_type}") if service_type == 'processing': result = processor.process_image_from_base64(image_data) else: result = checker.check_image_from_base64(image_data) print(f"[{WORKER_ID}] Job {unique_id} completed") send_result_to_orchestrator(unique_id, 'completed', result) with status_lock: total_processed += 1 except ValueError as ve: error_msg = str(ve) print(f"[{WORKER_ID}] Job {unique_id} failed (ValueError): {error_msg}") send_result_to_orchestrator(unique_id, 'failed', { 'error': error_msg, 'error_type': 'validation_error' }) except Exception as e: error_msg = str(e) if not error_msg or error_msg == '': error_msg = "Processing failed due to an unknown error" print(f"[{WORKER_ID}] Job {unique_id} failed: {error_msg}") print(traceback.format_exc()) send_result_to_orchestrator(unique_id, 'failed', { 'error': error_msg, 'error_type': 'processing_error' }) finally: with status_lock: current_status = 'idle' current_job = None send_heartbeat() def send_result_to_orchestrator(unique_id, status, result): if not ORCHESTRATOR_URL: print(f"[{WORKER_ID}] No orchestrator URL") return try: payload = { 'unique_id': unique_id, 'worker_id': WORKER_ID, 'status': status, 'result': result } if status == 'failed': if isinstance(result, dict) and 'error' not in result: payload['result'] = {'error': 'Processing failed'} elif isinstance(result, str): payload['result'] = {'error': result} print(f"[{WORKER_ID}] Sending result for {unique_id} - Status: {status}") if status == 'failed': print(f"[{WORKER_ID}] Error message: {payload['result'].get('error', 'Unknown')}") response = requests.post( f"{ORCHESTRATOR_URL}/worker/result", json=payload, timeout=30 ) if response.status_code == 200: print(f"[{WORKER_ID}] Result sent for {unique_id}") else: print(f"[{WORKER_ID}] Result send failed: {response.status_code}") print(f"[{WORKER_ID}] Response: {response.text}") except Exception as e: print(f"[{WORKER_ID}] Result send error: {e}") import traceback traceback.print_exc() def send_heartbeat(): if not ORCHESTRATOR_URL: return try: with status_lock: heartbeat_data = { 'worker_id': WORKER_ID, 'status': current_status, 'url': WORKER_URL, 'total_processed': total_processed, 'current_job': current_job } response = requests.post( f"{ORCHESTRATOR_URL}/worker/heartbeat", json=heartbeat_data, timeout=10 ) if response.status_code == 200: print(f"[{WORKER_ID}] Heartbeat sent - Status: {heartbeat_data['status']}") else: print(f"[{WORKER_ID}] Heartbeat failed: {response.status_code}") except Exception as e: print(f"[{WORKER_ID}] Heartbeat error: {e}") def periodic_heartbeat(): while True: try: time.sleep(30) send_heartbeat() except Exception as e: print(f"[{WORKER_ID}] Periodic heartbeat error: {e}") time.sleep(30) print(f"[{WORKER_ID}] Starting heartbeat thread...") heartbeat_thread = threading.Thread(target=periodic_heartbeat, daemon=True) heartbeat_thread.start() if __name__ == '__main__': print(f"=" * 60) print(f"Worker: {WORKER_ID}") print(f"Orchestrator: {ORCHESTRATOR_URL if ORCHESTRATOR_URL else 'Not configured'}") print(f"Worker URL: {WORKER_URL if WORKER_URL else 'Not configured'}") print(f"=" * 60) if ORCHESTRATOR_URL: print(f"[{WORKER_ID}] Sending initial heartbeat...") send_heartbeat() else: print(f"[{WORKER_ID}] ⚠ Standalone mode") port = int(os.getenv('PORT', 7860)) print(f"[{WORKER_ID}] Starting Flask on port {port}...") app.run(host='0.0.0.0', port=port, threaded=True)