# # # # import numpy as np # # # # import cv2 # # # # from sklearn.metrics.pairwise import cosine_similarity # # # # from scipy.stats import weibull_min # # # # from tensorflow.keras.models import load_model # # # # from tensorflow.keras.applications.resnet import preprocess_input # # # # # from config import MODEL_PATH, IMG_SIZE, COSINE_WEIGHT, COSINE_THRESHOLD, HYBRID_THRESHOLD # # # # from config import Config # # # # from gallery import load_gallery # # # # import os # # # # from huggingface_hub import hf_hub_download # # # # MODEL_REPO = "Omamaa12/iris-models" # # # # model_path = hf_hub_download( # # # # repo_id=MODEL_REPO, # # # # filename="resnet50_imagenet.h5", # # # # token=os.getenv("HF_TOKEN") # # # # ) # # # # base_model = load_model(model_path) # # # # # Load model # # # # # base_model = load_model( Config.MODEL_PATH) # # # # # Load gallery # # # # gallery_data = load_gallery() # # # # gallery = gallery_data["gallery"] # # # # weibull_models = gallery_data["weibull_models"] # # # # mean_all = gallery_data["mean_all"] # # # # std_all = gallery_data["std_all"] # # # # # def embed_image(path): # # # # # img = cv2.imread(path) # # # # # h, w = img.shape[:2] # # # # # crop = img[h//4:3*h//4, w//4:3*w//4] # # # # # img = cv2.resize(crop, Config.IMG_SIZE) # # # # # if len(img.shape) == 2 or img.shape[2] == 1: # # # # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) # # # # # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # # # # # img_prep = preprocess_input(np.expand_dims(img.astype(np.float32), axis=0)) # # # # # emb = base_model.predict(img_prep, verbose=0).flatten() # # # # # emb = (emb - mean_all) / std_all # # # # # emb = emb / (np.linalg.norm(emb)+1e-10) # # # # # return emb # # # # # def predict_hybrid(vec): # # # # # sims = {c: cosine_similarity(emb, vec.reshape(1,-1)).max() for c, emb in gallery.items()} # # # # # pred_class = max(sims, key=sims.get) # # # # # cosine_score = sims[pred_class] # # # # # if cosine_score < Config.COSINE_THRESHOLD: # # # # # return "unknown", cosine_score # # # # # mean_vec = gallery[pred_class].mean(axis=0) # # # # # dist = np.linalg.norm(vec - mean_vec) # # # # # shape, loc, scale = weibull_models[pred_class] # # # # # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # # # # # hybrid = Config.COSINE_WEIGHT * cosine_score + (1 - Config.COSINE_WEIGHT) * evt_prob # # # # # if hybrid < Config.HYBRID_THRESHOLD: # # # # # return "unknown", hybrid # # # # # return pred_class, hybrid # # # # def embed_image(path): # # # # img = cv2.imread(path) # # # # if img is None: # # # # raise ValueError(f"Cannot read image: {path}") # # # # h, w = img.shape[:2] # # # # crop = img[h//4:3*h//4, w//4:3*w//4] # # # # img = cv2.resize(crop, Config.IMG_SIZE) # # # # if len(img.shape) == 2 or img.shape[2] == 1: # # # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) # # # # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # # # # img_prep = preprocess_input(np.expand_dims(img.astype(np.float32), axis=0)) # # # # emb = base_model.predict(img_prep, verbose=0).flatten() # # # # emb = (emb - mean_all) / std_all # # # # emb = emb / (np.linalg.norm(emb) + 1e-10) # # # # return emb # # # # def predict_hybrid(vec): # # # # sims = { # # # # c: cosine_similarity(emb, vec.reshape(1, -1)).max() # # # # for c, emb in gallery.items() # # # # } # # # # pred_class = max(sims, key=sims.get) # # # # cosine_score = sims[pred_class] # # # # if cosine_score < Config.COSINE_THRESHOLD: # # # # return "unknown", cosine_score # # # # mean_vec = gallery[pred_class].mean(axis=0) # # # # dist = np.linalg.norm(vec - mean_vec) # # # # shape, loc, scale = weibull_models[pred_class] # # # # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # # # # hybrid = Config.COSINE_WEIGHT * cosine_score + (1 - Config.COSINE_WEIGHT) * evt_prob # # # # if hybrid < Config.HYBRID_THRESHOLD: # # # # return "unknown", hybrid # # # # return pred_class, hybrid # # # # import os # # # # import numpy as np # # # # import cv2 # # # # from sklearn.metrics.pairwise import cosine_similarity # # # # from scipy.stats import weibull_min # # # # from tensorflow.keras.models import load_model # # # # from tensorflow.keras.applications.resnet import preprocess_input # # # # from huggingface_hub import hf_hub_download # # # # from config import Config # # # # from gallery import load_gallery # # # import os # # # import numpy as np # # # import cv2 # # # from sklearn.metrics.pairwise import cosine_similarity # # # from scipy.stats import weibull_min # # # from tensorflow.keras.applications import ResNet50 # ← add this # # # from tensorflow.keras.applications.resnet import preprocess_input # # # from huggingface_hub import hf_hub_download # # # from config import Config # # # from gallery import load_gallery # # # # ───────────────────────────────────────── # # # # HuggingFace repo (set HF_TOKEN env var if repo is private) # # # # ───────────────────────────────────────── # # # MODEL_REPO = "Omamaa12/iris-models" # # # HF_TOKEN = os.getenv("HF_TOKEN") # # # # ───────────────────────────────────────── # # # # Load ResNet50 from HuggingFace # # # # ───────────────────────────────────────── # # # # print("⏳ Downloading ResNet50 from HuggingFace…") # # # # _resnet_path = hf_hub_download( # # # # repo_id=MODEL_REPO, # # # # filename="resnet50_imagenet.keras", # # # # # token=HF_TOKEN, # # # # ) # # # # # base_model = load_model(_resnet_path) # # # # # base_model = load_model(_resnet_path, compile=False, safe_mode=False) # # # # base_model = load_model( # # # # _resnet_path, # # # # compile=False, # # # # custom_objects={} # # # # ) # # # # print("✅ ResNet50 ready.") # # # print("⏳ Building ResNet50 with ImageNet weights…") # # # base_model = ResNet50(weights='imagenet', include_top=False, pooling='avg') # # # print("✅ ResNet50 ready.") # # # # ───────────────────────────────────────── # # # # Load gallery from HuggingFace # # # # ───────────────────────────────────────── # # # print("⏳ Downloading iris gallery from HuggingFace…") # # # _gallery_path = hf_hub_download( # # # repo_id=MODEL_REPO, # # # filename="iris_gallery_fixed.pkl", # # # token=HF_TOKEN, # # # ) # # # # Temporarily point Config.GALLERY_PATH to the downloaded file so # # # # gallery.py's load_gallery() can find it without modification. # # # Config.GALLERY_PATH = _gallery_path # # # gallery_data = load_gallery() # # # gallery = gallery_data["gallery"] # {class: np.array of normed embeddings} # # # weibull_models = gallery_data["weibull_models"] # {class: (shape, loc, scale)} # # # mean_all = gallery_data["mean_all"] # # # std_all = gallery_data["std_all"] # # # print(f"✅ Gallery loaded — {len(gallery)} identities.") # # # # ───────────────────────────────────────── # # # # Feature extraction # # # # ───────────────────────────────────────── # # # def embed_image(path): # # # img = cv2.imread(path) # # # if img is None: # # # raise ValueError(f"Cannot read image: {path}") # # # h, w = img.shape[:2] # # # crop = img[h//4:3*h//4, w//4:3*w//4] # # # img = cv2.resize(crop, Config.IMG_SIZE) # # # # if len(img.shape) == 2 or img.shape[2] == 1: # # # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) # # # if len(img.shape) == 2 or (len(img.shape) == 3 and img.shape[2] == 1): # # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # make it 3ch BGR first # # # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # # # img_arr = preprocess_input(np.expand_dims(img.astype(np.float32), axis=0)) # # # emb = base_model.predict(img_arr, verbose=0).flatten() # # # emb = (emb - mean_all) / std_all # # # emb = emb / (np.linalg.norm(emb) + 1e-10) # # # return emb # # # # ───────────────────────────────────────── # # # # Hybrid prediction (cosine + Weibull EVT) # # # # ───────────────────────────────────────── # # # def predict_hybrid(vec): # # # sims = { # # # c: cosine_similarity(emb, vec.reshape(1, -1)).max() # # # for c, emb in gallery.items() # # # } # # # pred_class = max(sims, key=sims.get) # # # cosine_score = sims[pred_class] # # # if cosine_score < Config.COSINE_THRESHOLD: # # # return "unknown", cosine_score # # # mean_vec = gallery[pred_class].mean(axis=0) # # # dist = np.linalg.norm(vec - mean_vec) # # # shape, loc, scale = weibull_models[pred_class] # # # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # # # hybrid = Config.COSINE_WEIGHT * cosine_score + (1 - Config.COSINE_WEIGHT) * evt_prob # # # if hybrid < Config.HYBRID_THRESHOLD: # # # return "unknown", hybrid # # # return pred_class, hybrid # # import os # # import cv2 # # import time # # import csv # # import pickle # # import numpy as np # # from collections import defaultdict # # from datetime import datetime # # from scipy.stats import weibull_min # # from sklearn.metrics.pairwise import cosine_similarity # # from tensorflow.keras.applications import ResNet50 # # from tensorflow.keras.applications.resnet import preprocess_input # # from huggingface_hub import hf_hub_download, HfApi # # # ============================== # # # MODEL LOAD # # # ============================== # # model = ResNet50(weights='imagenet', include_top=False, pooling='avg') # # os.makedirs('static/debug', exist_ok=True) # # # ============================== # # # PREPROCESSING # # # ============================== # # IMG_SIZE = (224, 224) # # def normalize_lighting(img): # # """ # # Standard illumination normalization to maintain gallery compatibility. # # """ # # if img is None: return None # # gray_mean = np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) # # gamma = np.log(128) / (np.log(gray_mean + 1e-5)) # # gamma = np.clip(gamma, 0.4, 2.5) # # lut = np.array([((i / 255.0) ** (1.0 / gamma)) * 255 for i in range(256)], dtype=np.uint8) # # img = cv2.LUT(img, lut) # # lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) # # l, a, b = cv2.split(lab) # # clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # # l = clahe.apply(l) # # lab = cv2.merge((l, a, b)) # # img = cv2.cvtColor(lab, cv2.COLOR_Lab2BGR) # # return img # # def _sharpen(img): # # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # # return cv2.filter2D(img, -1, kernel) # # def _high_contrast(img): # # lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) # # l, a, b = cv2.split(lab) # # # Match the registration limit (5.0) # # clahe = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # # l = clahe.apply(l) # # return cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_Lab2BGR) # # def preprocess_iris(img): # # if img is None: # # return None # # # 1. INITIAL CROP (Middle 50%) # # h, w = img.shape[:2] # # img = img[h // 4: 3 * h // 4, w // 4: 3 * w // 4] # # cv2.imwrite('static/debug/1_initial_crop.png', img) # # if len(img.shape) == 2 or img.shape[2] == 1: # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # # # 2. LIGHTING NORMALIZATION # # img = normalize_lighting(img) # # cv2.imwrite('static/debug/2_normalized.png', img) # # # 3. FINAL RESIZE # # img = cv2.resize(img, IMG_SIZE) # # cv2.imwrite('static/debug/3_final_input.png', img) # # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # # return img # # # ============================== # # # EMBEDDING # # # ============================== # # # def augment_lighting_variants(img): # # # """ # # # Creates a diverse set of environmental variants for registration. # # # These are added to the gallery ONLY for new registrations. # # # """ # # # variants = [img] # # # # 1. Brightness variants (stronger range) # # # variants.append(np.clip(img.astype(np.float32) * 1.6, 0, 255).astype(np.uint8)) # # # variants.append(np.clip(img.astype(np.float32) * 0.4, 0, 255).astype(np.uint8)) # # # # 2. High Contrast (High CLAHE) # # # lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) # # # l, a, b = cv2.split(lab) # # # clahe_high = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # # # l = clahe_high.apply(l) # # # lab = cv2.merge((l, a, b)) # # # variants.append(cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB)) # # # # 3. Sharpening (Added as an augmentation variant only) # # # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # # # sharpened = cv2.filter2D(img, -1, kernel) # # # variants.append(sharpened) # # # # 4. Blur variant (simulates slight out-of-focus) # # # variants.append(cv2.GaussianBlur(img, (3, 3), 0)) # # # # 5. Noise variant (simulates sensor noise) # # # noise = np.random.normal(0, 8, img.shape).astype(np.int16) # # # variants.append(np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8)) # # # return variants # # def augment_lighting_variants(img): # # """ # # Creates a diverse set of environmental variants for registration. # # These are added to the gallery ONLY for new registrations. # # """ # # variants = [] # # # Ensure debug directory exists # # os.makedirs('static/debug', exist_ok=True) # # # 0. Original preprocessed image # # variants.append(img) # # cv2.imwrite('static/debug/reg_0_original.png', cv2.cvtColor(img, cv2.COLOR_RGB2BGR)) # Change: Save original # # # 1. Brightness variants (stronger range) # # bright = np.clip(img.astype(np.float32) * 1.6, 0, 255).astype(np.uint8) # # variants.append(bright) # # cv2.imwrite('static/debug/reg_1_bright.png', cv2.cvtColor(bright, cv2.COLOR_RGB2BGR)) # Change: Save bright variant # # dark = np.clip(img.astype(np.float32) * 0.4, 0, 255).astype(np.uint8) # # variants.append(dark) # # cv2.imwrite('static/debug/reg_1_dark.png', cv2.cvtColor(dark, cv2.COLOR_RGB2BGR)) # Change: Save dark variant # # # 2. High Contrast (High CLAHE) # # lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) # # l, a, b = cv2.split(lab) # # clahe_high = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # # l = clahe_high.apply(l) # # lab = cv2.merge((l, a, b)) # # hc = cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB) # # variants.append(hc) # # cv2.imwrite('static/debug/reg_2_high_contrast.png', cv2.cvtColor(hc, cv2.COLOR_RGB2BGR)) # Change: Save high contrast variant # # # 3. Sharpening (Added as an augmentation variant only) # # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # # sharpened = cv2.filter2D(img, -1, kernel) # # variants.append(sharpened) # # cv2.imwrite('static/debug/reg_3_sharpened.png', cv2.cvtColor(sharpened, cv2.COLOR_RGB2BGR)) # Change: Save sharpened variant # # # 4. Blur variant (simulates slight out-of-focus) # # blurred = cv2.GaussianBlur(img, (3, 3), 0) # # variants.append(blurred) # # cv2.imwrite('static/debug/reg_4_blurred.png', cv2.cvtColor(blurred, cv2.COLOR_RGB2BGR)) # Change: Save blurred variant # # # 5. Noise variant (simulates sensor noise) # # noise = np.random.normal(0, 8, img.shape).astype(np.int16) # # noisy = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8) # # variants.append(noisy) # # cv2.imwrite('static/debug/reg_5_noisy.png', cv2.cvtColor(noisy, cv2.COLOR_RGB2BGR)) # Change: Save noisy variant # # return variants # # def embed_array(img_rgb): # # arr = preprocess_input(np.expand_dims(img_rgb.astype(np.float32), axis=0)) # # return model.predict(arr, verbose=0).flatten() # # # ============================== # # # LOAD GALLERY (FROM HUGGING FACE) # # # ============================== # # HF_REPO_ID = "Omamaa12/iris-models" # # HF_FILENAME = "iris_gallery_robustt.pkl" # # PKL_PATH = os.path.join('models', HF_FILENAME) # # def sync_gallery_from_hf(): # # """Downloads the latest gallery from Hugging Face.""" # # print(f"⏳ Syncing gallery from Hugging Face ({HF_REPO_ID})...") # # try: # # # Download to the models folder # # downloaded_path = hf_hub_download( # # repo_id=HF_REPO_ID, # # filename=HF_FILENAME, # # repo_type="model", # # local_dir="models", # # local_dir_use_symlinks=False, # # force_download=True, # # ) # # print(f"✅ Gallery synced: {downloaded_path}") # # return downloaded_path # # except Exception as e: # # print(f"⚠️ HF Sync failed, using local fallback: {e}") # # return PKL_PATH # # # Sync on startup # # PKL_PATH = sync_gallery_from_hf() # # if os.path.exists(PKL_PATH): # # with open(PKL_PATH, 'rb') as f: # # data = pickle.load(f) # # gallery = data['gallery'] # # weibull_models = data['weibull_models'] # # mean_all = data['mean_all'] # # std_all = data['std_all'] # # print(f"✅ Gallery loaded - {len(gallery)} identities.") # # else: # # print("❌ Gallery file not found! Initializing empty.") # # gallery = {} # # weibull_models = {} # # mean_all = None # These should ideally be pre-set # # std_all = None # # # ============================== # # # EMBED IMAGE (LOGIN) # # # ============================== # # ANGLE_AUGS = (-12, -6, 0, 6, 12) # # def _embed_rgb(rgb_img): # # arr = preprocess_input(np.expand_dims(rgb_img.astype(np.float32), axis=0)) # # emb = model.predict(arr, verbose=0).flatten() # # emb = (emb - mean_all) / std_all # # emb = emb / (np.linalg.norm(emb) + 1e-10) # # return emb # # def embed_image(image_path): # # """ # # Extracts multiple embeddings (TTA variants). # # Returns a list of vectors. # # """ # # img = cv2.imread(image_path) # # if img is None: # # return None # # pp = preprocess_iris(img) # # if pp is None: # # return None # # # TTA variants: Standard, Sharpened, High Contrast # # s = _sharpen(pp) # # hc = _high_contrast(pp) # # cv2.imwrite('static/debug/tta_sharpened.png', s) # # cv2.imwrite('static/debug/tta_high_contrast.png', hc) # # tta_variants = [pp, s, hc] # # h, w = pp.shape[:2] # # center = (w // 2, h // 2) # # final_vectors = [] # # for v in tta_variants: # # embs = [] # # for angle in ANGLE_AUGS: # # M = cv2.getRotationMatrix2D(center, angle, 1.0) # # rot = cv2.warpAffine(v, M, (w, h), borderMode=cv2.BORDER_REFLECT_101) # # embs.append(_embed_rgb(rot)) # # # Average rotations for THIS variant # # v_emb = np.mean(np.stack(embs), axis=0) # # v_emb = v_emb / (np.linalg.norm(v_emb) + 1e-10) # # final_vectors.append(v_emb) # # return final_vectors # # # ============================== # # # PREDICTION # # # ============================== # # COSINE_THRESHOLD = 0.62 # # # COSINE_THRESHOLD = 0.67 # # COSINE_WEIGHT = 0.85 # # # HYBRID_THRESHOLD = 0.55 # # HYBRID_THRESHOLD = 0.63 # # TOP2_MARGIN = 0.005 # # def _class_similarity(class_embs, vec): # # sims = cosine_similarity(class_embs, vec.reshape(1, -1)).ravel() # # return float(np.mean(np.sort(sims)[-2:])) # # def predict_robust(vectors): # # """ # # Matches multiple TTA vectors and takes the BEST (MAX) similarity. # # """ # # best_identity = 'unknown' # # best_cosine = 0 # # best_hybrid = 0 # # best_second = 0 # # # Try each TTA variant # # for vec in vectors: # # sims = {c: _class_similarity(emb, vec) for c, emb in gallery.items()} # # sorted_sims = sorted(sims.items(), key=lambda x: x[1], reverse=True) # # current_class, current_cos = sorted_sims[0] # # current_second = sorted_sims[1][1] if len(sorted_sims) > 1 else 0 # # mean_vec = gallery[current_class].mean(axis=0) # # dist = np.linalg.norm(vec - mean_vec) # # shape, loc, scale = weibull_models[current_class] # # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # # current_hybrid = COSINE_WEIGHT * current_cos + (1 - COSINE_WEIGHT) * evt_prob # # # If THIS variant is better than our previous best, update # # if current_cos > best_cosine: # # best_identity = current_class # # best_cosine = current_cos # # best_hybrid = current_hybrid # # best_second = current_second # # # LOGGING FOR DEBUGGING # # print(f"\n--- TTA Max-Score Debug ---") # # print(f"Final Predicted: {best_identity}") # # print(f"Max Cosine Score: {best_cosine:.4f} (Threshold: {COSINE_THRESHOLD})") # # print(f"Hybrid Score: {best_hybrid:.4f} (Threshold: {HYBRID_THRESHOLD})") # # print(f"---------------------------\n") # # if best_cosine < COSINE_THRESHOLD: # # return 'unknown', best_cosine, 0 # # if best_cosine - best_second < TOP2_MARGIN: # # return 'unknown', best_cosine, 0 # # if best_hybrid < HYBRID_THRESHOLD: # # return 'unknown', best_cosine, best_hybrid # # return best_identity, best_cosine, best_hybrid # # # ============================== # # # LOGIN SYSTEM # # # ============================== # # attempt_log = defaultdict(list) # # def login(image_path, session_id='default', silent=False): # # vec = embed_image(image_path) # # if vec is None: # # return {'status': 'error'} # # identity, cos, hyb = predict_robust(vec) # # if identity == 'unknown': # # return {'status': 'denied', 'identity': None, 'score': cos} # # return {'status': 'granted', 'identity': identity, 'score': hyb} # # # ============================== # # # REGISTRATION (UNCHANGED) # # # ============================== # # def _embed_for_registration(image_path): # # img = cv2.imread(image_path) # # if img is None: # # return [] # # pp = preprocess_iris(img) # # if pp is None: # # return [] # # embs = [] # # for v in augment_lighting_variants(pp): # # emb = embed_array(v) # # emb = (emb - mean_all) / (std_all + 1e-10) # # emb = emb / (np.linalg.norm(emb) + 1e-10) # # embs.append(emb) # # return embs # # # def register_person(person_label, image_paths, gallery_pkl_path=PKL_PATH, overwrite=False): # # # global gallery, weibull_models # # # new_embs = [] # # # for p in image_paths: # # # new_embs.extend(_embed_for_registration(p)) # # # new_embs = np.asarray(new_embs) # # # if person_label in gallery: # # # gallery[person_label] = np.vstack([gallery[person_label], new_embs]) # # # else: # # # gallery[person_label] = new_embs # # # # weibull_models[person_label] = (1, 0, 1) # # # # FIXED — real Weibull fit on actual embedding distances # # # mean_vec = gallery[person_label].mean(axis=0) # # # dists = np.linalg.norm(gallery[person_label] - mean_vec, axis=1) # # # if len(dists) >= 3: # # # # Need at least 3 points to fit Weibull reliably # # # tail_size = max(3, int(0.3 * len(dists))) # use top 30% of distances # # # tail = np.sort(dists)[-tail_size:] # # # shape, loc, scale = weibull_min.fit(tail, floc=0) # # # weibull_models[person_label] = (shape, loc, scale) # # # print(f"✅ Weibull fitted for {person_label}: shape={shape:.3f}, scale={scale:.3f}") # # # else: # # # # Fallback if somehow less than 3 embeddings # # # weibull_models[person_label] = (1, 0, 1) # # # print(f"⚠️ Not enough embeddings for Weibull fit, using fallback") # # # with open(gallery_pkl_path, 'wb') as f: # # # pickle.dump({ # # # 'gallery': gallery, # # # 'weibull_models': weibull_models, # # # 'mean_all': mean_all, # # # 'std_all': std_all # # # }, f) # # # # UPLOAD TO HUGGING FACE # # # try: # # # print(f"📤 Uploading updated gallery to Hugging Face...") # # # api = HfApi() # # # api.upload_file( # # # path_or_fileobj=gallery_pkl_path, # # # path_in_repo=HF_FILENAME, # # # repo_id=HF_REPO_ID, # # # repo_type="model" # # # ) # # # print("✅ Gallery updated on Hugging Face!") # # # except Exception as e: # # # print(f"❌ Failed to upload to Hugging Face: {e}") # # # return {'status': 'success', 'identity': person_label} # # def register_person(person_label, image_paths, gallery_pkl_path=PKL_PATH, overwrite=False): # # global gallery, weibull_models # # new_embs = [] # # for p in image_paths: # # new_embs.extend(_embed_for_registration(p)) # # new_embs = np.asarray(new_embs) # # if person_label in gallery: # # gallery[person_label] = np.vstack([gallery[person_label], new_embs]) # # else: # # gallery[person_label] = new_embs # # # FIXED — real Weibull fit on actual embedding distances # # mean_vec = gallery[person_label].mean(axis=0) # # dists = np.linalg.norm(gallery[person_label] - mean_vec, axis=1) # # if len(dists) >= 3: # # # Need at least 3 points to fit Weibull reliably # # tail_size = max(3, int(0.3 * len(dists))) # # tail = np.sort(dists)[-tail_size:] # # shape, loc, scale = weibull_min.fit(tail, floc=0) # # weibull_models[person_label] = (shape, loc, scale) # # print(f"✅ Weibull fitted for {person_label}: shape={shape:.3f}, scale={scale:.3f}") # # else: # # # Fallback if somehow less than 3 embeddings # # weibull_models[person_label] = (1, 0, 1) # # print(f"⚠️ Not enough embeddings for Weibull fit, using fallback") # # with open(gallery_pkl_path, 'wb') as f: # # pickle.dump({ # # 'gallery': gallery, # # 'weibull_models': weibull_models, # # 'mean_all': mean_all, # # 'std_all': std_all # # }, f) # # # UPLOAD TO HUGGING FACE # # try: # # print(f"📤 Uploading updated gallery to Hugging Face...") # # api = HfApi() # # api.upload_file( # # path_or_fileobj=gallery_pkl_path, # # path_in_repo=HF_FILENAME, # # repo_id=HF_REPO_ID, # # repo_type="model" # # ) # # print("✅ Gallery updated on Hugging Face!") # # except Exception as e: # # print(f"❌ Failed to upload to Hugging Face: {e}") # # return {'status': 'success', 'identity': person_label} # # print("✅ iris_recognition ready") # import os # import cv2 # import time # import csv # import pickle # import numpy as np # from collections import defaultdict # from datetime import datetime # from scipy.stats import weibull_min # from sklearn.metrics.pairwise import cosine_similarity # from tensorflow.keras.applications import ResNet50 # from tensorflow.keras.applications.resnet import preprocess_input # from huggingface_hub import hf_hub_download, HfApi # # ============================== # # MODEL LOAD # # ============================== # model = ResNet50(weights='imagenet', include_top=False, pooling='avg') # os.makedirs('static/debug', exist_ok=True) # # ============================== # # PREPROCESSING # # ============================== # IMG_SIZE = (224, 224) # def normalize_lighting(img): # """ # Standard illumination normalization to maintain gallery compatibility. # """ # if img is None: return None # gray_mean = np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) # gamma = np.log(128) / (np.log(gray_mean + 1e-5)) # gamma = np.clip(gamma, 0.4, 2.5) # lut = np.array([((i / 255.0) ** (1.0 / gamma)) * 255 for i in range(256)], dtype=np.uint8) # img = cv2.LUT(img, lut) # lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) # l, a, b = cv2.split(lab) # clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # l = clahe.apply(l) # lab = cv2.merge((l, a, b)) # img = cv2.cvtColor(lab, cv2.COLOR_Lab2BGR) # return img # def _sharpen(img): # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # return cv2.filter2D(img, -1, kernel) # def _high_contrast(img): # lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) # l, a, b = cv2.split(lab) # # Match the registration limit (5.0) # clahe = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # l = clahe.apply(l) # return cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_Lab2BGR) # def preprocess_iris(img): # if img is None: # return None # # 1. INITIAL CROP (Middle 50%) # h, w = img.shape[:2] # img = img[h // 4: 3 * h // 4, w // 4: 3 * w // 4] # cv2.imwrite('static/debug/1_initial_crop.png', img) # if len(img.shape) == 2 or img.shape[2] == 1: # img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # # 2. LIGHTING NORMALIZATION # img = normalize_lighting(img) # cv2.imwrite('static/debug/2_normalized.png', img) # # 3. FINAL RESIZE # img = cv2.resize(img, IMG_SIZE) # cv2.imwrite('static/debug/3_final_input.png', img) # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # return img # # ============================== # # EMBEDDING # # ============================== # # def augment_lighting_variants(img): # # """ # # Creates a diverse set of environmental variants for registration. # # These are added to the gallery ONLY for new registrations. # # """ # # variants = [img] # # # 1. Brightness variants (stronger range) # # variants.append(np.clip(img.astype(np.float32) * 1.6, 0, 255).astype(np.uint8)) # # variants.append(np.clip(img.astype(np.float32) * 0.4, 0, 255).astype(np.uint8)) # # # 2. High Contrast (High CLAHE) # # lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) # # l, a, b = cv2.split(lab) # # clahe_high = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # # l = clahe_high.apply(l) # # lab = cv2.merge((l, a, b)) # # variants.append(cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB)) # # # 3. Sharpening (Added as an augmentation variant only) # # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # # sharpened = cv2.filter2D(img, -1, kernel) # # variants.append(sharpened) # # # 4. Blur variant (simulates slight out-of-focus) # # variants.append(cv2.GaussianBlur(img, (3, 3), 0)) # # # 5. Noise variant (simulates sensor noise) # # noise = np.random.normal(0, 8, img.shape).astype(np.int16) # # variants.append(np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8)) # # return variants # def augment_lighting_variants(img): # """ # Creates a diverse set of environmental variants for registration. # These are added to the gallery ONLY for new registrations. # """ # variants = [] # # Ensure debug directory exists # os.makedirs('static/debug', exist_ok=True) # # 0. Original preprocessed image # variants.append(img) # cv2.imwrite('static/debug/reg_0_original.png', cv2.cvtColor(img, cv2.COLOR_RGB2BGR)) # Change: Save original # # 1. Brightness variants (stronger range) # bright = np.clip(img.astype(np.float32) * 1.6, 0, 255).astype(np.uint8) # variants.append(bright) # cv2.imwrite('static/debug/reg_1_bright.png', cv2.cvtColor(bright, cv2.COLOR_RGB2BGR)) # Change: Save bright variant # dark = np.clip(img.astype(np.float32) * 0.4, 0, 255).astype(np.uint8) # variants.append(dark) # cv2.imwrite('static/debug/reg_1_dark.png', cv2.cvtColor(dark, cv2.COLOR_RGB2BGR)) # Change: Save dark variant # # 2. High Contrast (High CLAHE) # lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) # l, a, b = cv2.split(lab) # clahe_high = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # l = clahe_high.apply(l) # lab = cv2.merge((l, a, b)) # hc = cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB) # variants.append(hc) # cv2.imwrite('static/debug/reg_2_high_contrast.png', cv2.cvtColor(hc, cv2.COLOR_RGB2BGR)) # Change: Save high contrast variant # # 3. Sharpening (Added as an augmentation variant only) # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # sharpened = cv2.filter2D(img, -1, kernel) # variants.append(sharpened) # cv2.imwrite('static/debug/reg_3_sharpened.png', cv2.cvtColor(sharpened, cv2.COLOR_RGB2BGR)) # Change: Save sharpened variant # # 4. Blur variant (simulates slight out-of-focus) # blurred = cv2.GaussianBlur(img, (3, 3), 0) # variants.append(blurred) # cv2.imwrite('static/debug/reg_4_blurred.png', cv2.cvtColor(blurred, cv2.COLOR_RGB2BGR)) # Change: Save blurred variant # # 5. Noise variant (simulates sensor noise) # noise = np.random.normal(0, 8, img.shape).astype(np.int16) # noisy = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8) # variants.append(noisy) # cv2.imwrite('static/debug/reg_5_noisy.png', cv2.cvtColor(noisy, cv2.COLOR_RGB2BGR)) # Change: Save noisy variant # return variants # def embed_array(img_rgb): # arr = preprocess_input(np.expand_dims(img_rgb.astype(np.float32), axis=0)) # return model.predict(arr, verbose=0).flatten() # # ============================== # # LOAD GALLERY (FROM HUGGING FACE) # # ============================== # HF_REPO_ID = "Omamaa12/iris-models" # HF_FILENAME = "iris_gallery_robustt.pkl" # PKL_PATH = os.path.join('models', HF_FILENAME) # def sync_gallery_from_hf(): # """Downloads the latest gallery from Hugging Face.""" # print(f"⏳ Syncing gallery from Hugging Face ({HF_REPO_ID})...") # try: # # Download to the models folder # downloaded_path = hf_hub_download( # repo_id=HF_REPO_ID, # filename=HF_FILENAME, # repo_type="model", # local_dir="models", # local_dir_use_symlinks=False, # force_download=True, # ) # print(f"✅ Gallery synced: {downloaded_path}") # return downloaded_path # except Exception as e: # print(f"⚠️ HF Sync failed, using local fallback: {e}") # return PKL_PATH # # Sync on startup # PKL_PATH = sync_gallery_from_hf() # if os.path.exists(PKL_PATH): # with open(PKL_PATH, 'rb') as f: # data = pickle.load(f) # gallery = data['gallery'] # weibull_models = data['weibull_models'] # mean_all = data['mean_all'] # std_all = data['std_all'] # print(f"✅ Gallery loaded - {len(gallery)} identities.") # else: # print("❌ Gallery file not found! Initializing empty.") # gallery = {} # weibull_models = {} # mean_all = None # These should ideally be pre-set # std_all = None # # ============================== # # EMBED IMAGE (LOGIN) # # ============================== # ANGLE_AUGS = (-12, -6, 0, 6, 12) # def _embed_rgb(rgb_img): # arr = preprocess_input(np.expand_dims(rgb_img.astype(np.float32), axis=0)) # emb = model.predict(arr, verbose=0).flatten() # emb = (emb - mean_all) / (std_all + 1e-10) # ← ADD THIS LINE BACK # emb = emb / (np.linalg.norm(emb) + 1e-10) # return emb # def embed_image(image_path): # """ # Extracts multiple embeddings (TTA variants). # Returns a list of vectors. # """ # img = cv2.imread(image_path) # if img is None: # return None # pp = preprocess_iris(img) # if pp is None: # return None # # TTA variants: Standard, Sharpened, High Contrast # s = _sharpen(pp) # hc = _high_contrast(pp) # cv2.imwrite('static/debug/tta_sharpened.png', s) # cv2.imwrite('static/debug/tta_high_contrast.png', hc) # tta_variants = [pp, s, hc] # h, w = pp.shape[:2] # center = (w // 2, h // 2) # final_vectors = [] # for v in tta_variants: # embs = [] # for angle in ANGLE_AUGS: # M = cv2.getRotationMatrix2D(center, angle, 1.0) # rot = cv2.warpAffine(v, M, (w, h), borderMode=cv2.BORDER_REFLECT_101) # embs.append(_embed_rgb(rot)) # # Average rotations for THIS variant # v_emb = np.mean(np.stack(embs), axis=0) # v_emb = v_emb / (np.linalg.norm(v_emb) + 1e-10) # final_vectors.append(v_emb) # return final_vectors # # ============================== # # PREDICTION # # ============================== # COSINE_THRESHOLD = 0.62 # # COSINE_THRESHOLD = 0.67 # COSINE_WEIGHT = 0.85 # # HYBRID_THRESHOLD = 0.55 # HYBRID_THRESHOLD = 0.63 # TOP2_MARGIN = 0.005 # def _class_similarity(class_embs, vec): # sims = cosine_similarity(class_embs, vec.reshape(1, -1)).ravel() # return float(np.mean(np.sort(sims)[-2:])) # def predict_robust(vectors): # """ # Matches multiple TTA vectors and takes the BEST (MAX) similarity. # """ # best_identity = 'unknown' # best_cosine = 0 # best_hybrid = 0 # best_second = 0 # # Try each TTA variant # for vec in vectors: # sims = {c: _class_similarity(emb, vec) for c, emb in gallery.items()} # sorted_sims = sorted(sims.items(), key=lambda x: x[1], reverse=True) # current_class, current_cos = sorted_sims[0] # current_second = sorted_sims[1][1] if len(sorted_sims) > 1 else 0 # mean_vec = gallery[current_class].mean(axis=0) # dist = np.linalg.norm(vec - mean_vec) # shape, loc, scale = weibull_models[current_class] # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # current_hybrid = COSINE_WEIGHT * current_cos + (1 - COSINE_WEIGHT) * evt_prob # # If THIS variant is better than our previous best, update # if current_cos > best_cosine: # best_identity = current_class # best_cosine = current_cos # best_hybrid = current_hybrid # best_second = current_second # # LOGGING FOR DEBUGGING # print(f"\n--- TTA Max-Score Debug ---") # print(f"Final Predicted: {best_identity}") # print(f"Max Cosine Score: {best_cosine:.4f} (Threshold: {COSINE_THRESHOLD})") # print(f"Hybrid Score: {best_hybrid:.4f} (Threshold: {HYBRID_THRESHOLD})") # print(f"---------------------------\n") # if best_cosine < COSINE_THRESHOLD: # return 'unknown', best_cosine, 0 # if best_cosine - best_second < TOP2_MARGIN: # return 'unknown', best_cosine, 0 # if best_hybrid < HYBRID_THRESHOLD: # return 'unknown', best_cosine, best_hybrid # return best_identity, best_cosine, best_hybrid # # ============================== # # LOGIN SYSTEM # # ============================== # attempt_log = defaultdict(list) # def login(image_path, session_id='default', silent=False): # vec = embed_image(image_path) # if vec is None: # return {'status': 'error'} # identity, cos, hyb = predict_robust(vec) # if identity == 'unknown': # return {'status': 'denied', 'identity': None, 'score': cos} # return {'status': 'granted', 'identity': identity, 'score': hyb} # # ============================== # # REGISTRATION (UNCHANGED) # # ============================== # def _embed_for_registration(image_path): # img = cv2.imread(image_path) # if img is None: # return [] # pp = preprocess_iris(img) # if pp is None: # return [] # embs = [] # for v in augment_lighting_variants(pp): # emb = embed_array(v) # # Match gallery format: only L2 normalize, no mean_all/std_all standardization # emb = emb / (np.linalg.norm(emb) + 1e-10) # embs.append(emb) # return embs # # def register_person(person_label, image_paths, gallery_pkl_path=PKL_PATH, overwrite=False): # # global gallery, weibull_models # # new_embs = [] # # for p in image_paths: # # new_embs.extend(_embed_for_registration(p)) # # new_embs = np.asarray(new_embs) # # if person_label in gallery: # # gallery[person_label] = np.vstack([gallery[person_label], new_embs]) # # else: # # gallery[person_label] = new_embs # # # weibull_models[person_label] = (1, 0, 1) # # # FIXED — real Weibull fit on actual embedding distances # # mean_vec = gallery[person_label].mean(axis=0) # # dists = np.linalg.norm(gallery[person_label] - mean_vec, axis=1) # # if len(dists) >= 3: # # # Need at least 3 points to fit Weibull reliably # # tail_size = max(3, int(0.3 * len(dists))) # use top 30% of distances # # tail = np.sort(dists)[-tail_size:] # # shape, loc, scale = weibull_min.fit(tail, floc=0) # # weibull_models[person_label] = (shape, loc, scale) # # print(f"✅ Weibull fitted for {person_label}: shape={shape:.3f}, scale={scale:.3f}") # # else: # # # Fallback if somehow less than 3 embeddings # # weibull_models[person_label] = (1, 0, 1) # # print(f"⚠️ Not enough embeddings for Weibull fit, using fallback") # # with open(gallery_pkl_path, 'wb') as f: # # pickle.dump({ # # 'gallery': gallery, # # 'weibull_models': weibull_models, # # 'mean_all': mean_all, # # 'std_all': std_all # # }, f) # # # UPLOAD TO HUGGING FACE # # try: # # print(f"📤 Uploading updated gallery to Hugging Face...") # # api = HfApi() # # api.upload_file( # # path_or_fileobj=gallery_pkl_path, # # path_in_repo=HF_FILENAME, # # repo_id=HF_REPO_ID, # # repo_type="model" # # ) # # print("✅ Gallery updated on Hugging Face!") # # except Exception as e: # # print(f"❌ Failed to upload to Hugging Face: {e}") # # return {'status': 'success', 'identity': person_label} # def register_person(person_label, image_paths, gallery_pkl_path=PKL_PATH, overwrite=False): # global gallery, weibull_models # new_embs = [] # for p in image_paths: # new_embs.extend(_embed_for_registration(p)) # new_embs = np.asarray(new_embs) # if person_label in gallery: # gallery[person_label] = np.vstack([gallery[person_label], new_embs]) # else: # gallery[person_label] = new_embs # # FIXED — real Weibull fit on actual embedding distances # mean_vec = gallery[person_label].mean(axis=0) # dists = np.linalg.norm(gallery[person_label] - mean_vec, axis=1) # if len(dists) >= 3: # # Need at least 3 points to fit Weibull reliably # tail_size = max(3, int(0.3 * len(dists))) # tail = np.sort(dists)[-tail_size:] # shape, loc, scale = weibull_min.fit(tail, floc=0) # weibull_models[person_label] = (shape, loc, scale) # print(f"✅ Weibull fitted for {person_label}: shape={shape:.3f}, scale={scale:.3f}") # else: # # Fallback if somehow less than 3 embeddings # weibull_models[person_label] = (1, 0, 1) # print(f"⚠️ Not enough embeddings for Weibull fit, using fallback") # with open(gallery_pkl_path, 'wb') as f: # pickle.dump({ # 'gallery': gallery, # 'weibull_models': weibull_models, # 'mean_all': mean_all, # 'std_all': std_all # }, f) # # UPLOAD TO HUGGING FACE # try: # print(f"📤 Uploading updated gallery to Hugging Face...") # api = HfApi() # api.upload_file( # path_or_fileobj=gallery_pkl_path, # path_in_repo=HF_FILENAME, # repo_id=HF_REPO_ID, # repo_type="model" # ) # print("✅ Gallery updated on Hugging Face!") # except Exception as e: # print(f"❌ Failed to upload to Hugging Face: {e}") # return {'status': 'success', 'identity': person_label} # print("✅ iris_recognition ready") # # # import numpy as np # # # import cv2 # # # from sklearn.metrics.pairwise import cosine_similarity # # # from scipy.stats import weibull_min # # # from tensorflow.keras.models import load_model # # # from tensorflow.keras.applications.resnet import preprocess_input # # # # from config import MODEL_PATH, IMG_SIZE, COSINE_WEIGHT, COSINE_THRESHOLD, HYBRID_THRESHOLD # # # from config import Config # # # from gallery import load_gallery # # # import os # # # from huggingface_hub import hf_hub_download # # # MODEL_REPO = "Omamaa12/iris-models" # # # model_path = hf_hub_download( # # # repo_id=MODEL_REPO, # # # filename="resnet50_imagenet.h5", # # # token=os.getenv("HF_TOKEN") # # # ) # # # base_model = load_model(model_path) # # # # Load model # # # # base_model = load_model( Config.MODEL_PATH) # # # # Load gallery # # # gallery_data = load_gallery() # # # gallery = gallery_data["gallery"] # # # weibull_models = gallery_data["weibull_models"] # # # mean_all = gallery_data["mean_all"] # # # std_all = gallery_data["std_all"] # # # # def embed_image(path): # # # # img = cv2.imread(path) # # # # h, w = img.shape[:2] # # # # crop = img[h//4:3*h//4, w//4:3*w//4] # # # # img = cv2.resize(crop, Config.IMG_SIZE) # # # # if len(img.shape) == 2 or img.shape[2] == 1: # # # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) # # # # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # # # # img_prep = preprocess_input(np.expand_dims(img.astype(np.float32), axis=0)) # # # # emb = base_model.predict(img_prep, verbose=0).flatten() # # # # emb = (emb - mean_all) / std_all # # # # emb = emb / (np.linalg.norm(emb)+1e-10) # # # # return emb # # # # def predict_hybrid(vec): # # # # sims = {c: cosine_similarity(emb, vec.reshape(1,-1)).max() for c, emb in gallery.items()} # # # # pred_class = max(sims, key=sims.get) # # # # cosine_score = sims[pred_class] # # # # if cosine_score < Config.COSINE_THRESHOLD: # # # # return "unknown", cosine_score # # # # mean_vec = gallery[pred_class].mean(axis=0) # # # # dist = np.linalg.norm(vec - mean_vec) # # # # shape, loc, scale = weibull_models[pred_class] # # # # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # # # # hybrid = Config.COSINE_WEIGHT * cosine_score + (1 - Config.COSINE_WEIGHT) * evt_prob # # # # if hybrid < Config.HYBRID_THRESHOLD: # # # # return "unknown", hybrid # # # # return pred_class, hybrid # # # def embed_image(path): # # # img = cv2.imread(path) # # # if img is None: # # # raise ValueError(f"Cannot read image: {path}") # # # h, w = img.shape[:2] # # # crop = img[h//4:3*h//4, w//4:3*w//4] # # # img = cv2.resize(crop, Config.IMG_SIZE) # # # if len(img.shape) == 2 or img.shape[2] == 1: # # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) # # # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # # # img_prep = preprocess_input(np.expand_dims(img.astype(np.float32), axis=0)) # # # emb = base_model.predict(img_prep, verbose=0).flatten() # # # emb = (emb - mean_all) / std_all # # # emb = emb / (np.linalg.norm(emb) + 1e-10) # # # return emb # # # def predict_hybrid(vec): # # # sims = { # # # c: cosine_similarity(emb, vec.reshape(1, -1)).max() # # # for c, emb in gallery.items() # # # } # # # pred_class = max(sims, key=sims.get) # # # cosine_score = sims[pred_class] # # # if cosine_score < Config.COSINE_THRESHOLD: # # # return "unknown", cosine_score # # # mean_vec = gallery[pred_class].mean(axis=0) # # # dist = np.linalg.norm(vec - mean_vec) # # # shape, loc, scale = weibull_models[pred_class] # # # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # # # hybrid = Config.COSINE_WEIGHT * cosine_score + (1 - Config.COSINE_WEIGHT) * evt_prob # # # if hybrid < Config.HYBRID_THRESHOLD: # # # return "unknown", hybrid # # # return pred_class, hybrid # # # import os # # # import numpy as np # # # import cv2 # # # from sklearn.metrics.pairwise import cosine_similarity # # # from scipy.stats import weibull_min # # # from tensorflow.keras.models import load_model # # # from tensorflow.keras.applications.resnet import preprocess_input # # # from huggingface_hub import hf_hub_download # # # from config import Config # # # from gallery import load_gallery # # import os # # import numpy as np # # import cv2 # # from sklearn.metrics.pairwise import cosine_similarity # # from scipy.stats import weibull_min # # from tensorflow.keras.applications import ResNet50 # ← add this # # from tensorflow.keras.applications.resnet import preprocess_input # # from huggingface_hub import hf_hub_download # # from config import Config # # from gallery import load_gallery # # # ───────────────────────────────────────── # # # HuggingFace repo (set HF_TOKEN env var if repo is private) # # # ───────────────────────────────────────── # # MODEL_REPO = "Omamaa12/iris-models" # # HF_TOKEN = os.getenv("HF_TOKEN") # # # ───────────────────────────────────────── # # # Load ResNet50 from HuggingFace # # # ───────────────────────────────────────── # # # print("⏳ Downloading ResNet50 from HuggingFace…") # # # _resnet_path = hf_hub_download( # # # repo_id=MODEL_REPO, # # # filename="resnet50_imagenet.keras", # # # # token=HF_TOKEN, # # # ) # # # # base_model = load_model(_resnet_path) # # # # base_model = load_model(_resnet_path, compile=False, safe_mode=False) # # # base_model = load_model( # # # _resnet_path, # # # compile=False, # # # custom_objects={} # # # ) # # # print("✅ ResNet50 ready.") # # print("⏳ Building ResNet50 with ImageNet weights…") # # base_model = ResNet50(weights='imagenet', include_top=False, pooling='avg') # # print("✅ ResNet50 ready.") # # # ───────────────────────────────────────── # # # Load gallery from HuggingFace # # # ───────────────────────────────────────── # # print("⏳ Downloading iris gallery from HuggingFace…") # # _gallery_path = hf_hub_download( # # repo_id=MODEL_REPO, # # filename="iris_gallery_fixed.pkl", # # token=HF_TOKEN, # # ) # # # Temporarily point Config.GALLERY_PATH to the downloaded file so # # # gallery.py's load_gallery() can find it without modification. # # Config.GALLERY_PATH = _gallery_path # # gallery_data = load_gallery() # # gallery = gallery_data["gallery"] # {class: np.array of normed embeddings} # # weibull_models = gallery_data["weibull_models"] # {class: (shape, loc, scale)} # # mean_all = gallery_data["mean_all"] # # std_all = gallery_data["std_all"] # # print(f"✅ Gallery loaded — {len(gallery)} identities.") # # # ───────────────────────────────────────── # # # Feature extraction # # # ───────────────────────────────────────── # # def embed_image(path): # # img = cv2.imread(path) # # if img is None: # # raise ValueError(f"Cannot read image: {path}") # # h, w = img.shape[:2] # # crop = img[h//4:3*h//4, w//4:3*w//4] # # img = cv2.resize(crop, Config.IMG_SIZE) # # # if len(img.shape) == 2 or img.shape[2] == 1: # # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) # # if len(img.shape) == 2 or (len(img.shape) == 3 and img.shape[2] == 1): # # img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # make it 3ch BGR first # # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # # img_arr = preprocess_input(np.expand_dims(img.astype(np.float32), axis=0)) # # emb = base_model.predict(img_arr, verbose=0).flatten() # # emb = (emb - mean_all) / std_all # # emb = emb / (np.linalg.norm(emb) + 1e-10) # # return emb # # # ───────────────────────────────────────── # # # Hybrid prediction (cosine + Weibull EVT) # # # ───────────────────────────────────────── # # def predict_hybrid(vec): # # sims = { # # c: cosine_similarity(emb, vec.reshape(1, -1)).max() # # for c, emb in gallery.items() # # } # # pred_class = max(sims, key=sims.get) # # cosine_score = sims[pred_class] # # if cosine_score < Config.COSINE_THRESHOLD: # # return "unknown", cosine_score # # mean_vec = gallery[pred_class].mean(axis=0) # # dist = np.linalg.norm(vec - mean_vec) # # shape, loc, scale = weibull_models[pred_class] # # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # # hybrid = Config.COSINE_WEIGHT * cosine_score + (1 - Config.COSINE_WEIGHT) * evt_prob # # if hybrid < Config.HYBRID_THRESHOLD: # # return "unknown", hybrid # # return pred_class, hybrid # import os # import cv2 # import time # import csv # import pickle # import numpy as np # from collections import defaultdict # from datetime import datetime # from scipy.stats import weibull_min # from sklearn.metrics.pairwise import cosine_similarity # from tensorflow.keras.applications import ResNet50 # from tensorflow.keras.applications.resnet import preprocess_input # from huggingface_hub import hf_hub_download, HfApi # # ============================== # # MODEL LOAD # # ============================== # model = ResNet50(weights='imagenet', include_top=False, pooling='avg') # os.makedirs('static/debug', exist_ok=True) # # ============================== # # PREPROCESSING # # ============================== # IMG_SIZE = (224, 224) # def normalize_lighting(img): # """ # Standard illumination normalization to maintain gallery compatibility. # """ # if img is None: return None # gray_mean = np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) # gamma = np.log(128) / (np.log(gray_mean + 1e-5)) # gamma = np.clip(gamma, 0.4, 2.5) # lut = np.array([((i / 255.0) ** (1.0 / gamma)) * 255 for i in range(256)], dtype=np.uint8) # img = cv2.LUT(img, lut) # lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) # l, a, b = cv2.split(lab) # clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) # l = clahe.apply(l) # lab = cv2.merge((l, a, b)) # img = cv2.cvtColor(lab, cv2.COLOR_Lab2BGR) # return img # def _sharpen(img): # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # return cv2.filter2D(img, -1, kernel) # def _high_contrast(img): # lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) # l, a, b = cv2.split(lab) # # Match the registration limit (5.0) # clahe = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # l = clahe.apply(l) # return cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_Lab2BGR) # def preprocess_iris(img): # if img is None: # return None # # 1. INITIAL CROP (Middle 50%) # h, w = img.shape[:2] # img = img[h // 4: 3 * h // 4, w // 4: 3 * w // 4] # cv2.imwrite('static/debug/1_initial_crop.png', img) # if len(img.shape) == 2 or img.shape[2] == 1: # img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # # 2. LIGHTING NORMALIZATION # img = normalize_lighting(img) # cv2.imwrite('static/debug/2_normalized.png', img) # # 3. FINAL RESIZE # img = cv2.resize(img, IMG_SIZE) # cv2.imwrite('static/debug/3_final_input.png', img) # img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # return img # # ============================== # # EMBEDDING # # ============================== # # def augment_lighting_variants(img): # # """ # # Creates a diverse set of environmental variants for registration. # # These are added to the gallery ONLY for new registrations. # # """ # # variants = [img] # # # 1. Brightness variants (stronger range) # # variants.append(np.clip(img.astype(np.float32) * 1.6, 0, 255).astype(np.uint8)) # # variants.append(np.clip(img.astype(np.float32) * 0.4, 0, 255).astype(np.uint8)) # # # 2. High Contrast (High CLAHE) # # lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) # # l, a, b = cv2.split(lab) # # clahe_high = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # # l = clahe_high.apply(l) # # lab = cv2.merge((l, a, b)) # # variants.append(cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB)) # # # 3. Sharpening (Added as an augmentation variant only) # # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # # sharpened = cv2.filter2D(img, -1, kernel) # # variants.append(sharpened) # # # 4. Blur variant (simulates slight out-of-focus) # # variants.append(cv2.GaussianBlur(img, (3, 3), 0)) # # # 5. Noise variant (simulates sensor noise) # # noise = np.random.normal(0, 8, img.shape).astype(np.int16) # # variants.append(np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8)) # # return variants # def augment_lighting_variants(img): # """ # Creates a diverse set of environmental variants for registration. # These are added to the gallery ONLY for new registrations. # """ # variants = [] # # Ensure debug directory exists # os.makedirs('static/debug', exist_ok=True) # # 0. Original preprocessed image # variants.append(img) # cv2.imwrite('static/debug/reg_0_original.png', cv2.cvtColor(img, cv2.COLOR_RGB2BGR)) # Change: Save original # # 1. Brightness variants (stronger range) # bright = np.clip(img.astype(np.float32) * 1.6, 0, 255).astype(np.uint8) # variants.append(bright) # cv2.imwrite('static/debug/reg_1_bright.png', cv2.cvtColor(bright, cv2.COLOR_RGB2BGR)) # Change: Save bright variant # dark = np.clip(img.astype(np.float32) * 0.4, 0, 255).astype(np.uint8) # variants.append(dark) # cv2.imwrite('static/debug/reg_1_dark.png', cv2.cvtColor(dark, cv2.COLOR_RGB2BGR)) # Change: Save dark variant # # 2. High Contrast (High CLAHE) # lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) # l, a, b = cv2.split(lab) # clahe_high = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # l = clahe_high.apply(l) # lab = cv2.merge((l, a, b)) # hc = cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB) # variants.append(hc) # cv2.imwrite('static/debug/reg_2_high_contrast.png', cv2.cvtColor(hc, cv2.COLOR_RGB2BGR)) # Change: Save high contrast variant # # 3. Sharpening (Added as an augmentation variant only) # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # sharpened = cv2.filter2D(img, -1, kernel) # variants.append(sharpened) # cv2.imwrite('static/debug/reg_3_sharpened.png', cv2.cvtColor(sharpened, cv2.COLOR_RGB2BGR)) # Change: Save sharpened variant # # 4. Blur variant (simulates slight out-of-focus) # blurred = cv2.GaussianBlur(img, (3, 3), 0) # variants.append(blurred) # cv2.imwrite('static/debug/reg_4_blurred.png', cv2.cvtColor(blurred, cv2.COLOR_RGB2BGR)) # Change: Save blurred variant # # 5. Noise variant (simulates sensor noise) # noise = np.random.normal(0, 8, img.shape).astype(np.int16) # noisy = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8) # variants.append(noisy) # cv2.imwrite('static/debug/reg_5_noisy.png', cv2.cvtColor(noisy, cv2.COLOR_RGB2BGR)) # Change: Save noisy variant # return variants # def embed_array(img_rgb): # arr = preprocess_input(np.expand_dims(img_rgb.astype(np.float32), axis=0)) # return model.predict(arr, verbose=0).flatten() # # ============================== # # LOAD GALLERY (FROM HUGGING FACE) # # ============================== # HF_REPO_ID = "Omamaa12/iris-models" # HF_FILENAME = "iris_gallery_robustt.pkl" # PKL_PATH = os.path.join('models', HF_FILENAME) # def sync_gallery_from_hf(): # """Downloads the latest gallery from Hugging Face.""" # print(f"⏳ Syncing gallery from Hugging Face ({HF_REPO_ID})...") # try: # # Download to the models folder # downloaded_path = hf_hub_download( # repo_id=HF_REPO_ID, # filename=HF_FILENAME, # repo_type="model", # local_dir="models", # local_dir_use_symlinks=False, # force_download=True, # ) # print(f"✅ Gallery synced: {downloaded_path}") # return downloaded_path # except Exception as e: # print(f"⚠️ HF Sync failed, using local fallback: {e}") # return PKL_PATH # # Sync on startup # PKL_PATH = sync_gallery_from_hf() # if os.path.exists(PKL_PATH): # with open(PKL_PATH, 'rb') as f: # data = pickle.load(f) # gallery = data['gallery'] # weibull_models = data['weibull_models'] # mean_all = data['mean_all'] # std_all = data['std_all'] # print(f"✅ Gallery loaded - {len(gallery)} identities.") # else: # print("❌ Gallery file not found! Initializing empty.") # gallery = {} # weibull_models = {} # mean_all = None # These should ideally be pre-set # std_all = None # # ============================== # # EMBED IMAGE (LOGIN) # # ============================== # ANGLE_AUGS = (-12, -6, 0, 6, 12) # def _embed_rgb(rgb_img): # arr = preprocess_input(np.expand_dims(rgb_img.astype(np.float32), axis=0)) # emb = model.predict(arr, verbose=0).flatten() # emb = (emb - mean_all) / std_all # emb = emb / (np.linalg.norm(emb) + 1e-10) # return emb # def embed_image(image_path): # """ # Extracts multiple embeddings (TTA variants). # Returns a list of vectors. # """ # img = cv2.imread(image_path) # if img is None: # return None # pp = preprocess_iris(img) # if pp is None: # return None # # TTA variants: Standard, Sharpened, High Contrast # s = _sharpen(pp) # hc = _high_contrast(pp) # cv2.imwrite('static/debug/tta_sharpened.png', s) # cv2.imwrite('static/debug/tta_high_contrast.png', hc) # tta_variants = [pp, s, hc] # h, w = pp.shape[:2] # center = (w // 2, h // 2) # final_vectors = [] # for v in tta_variants: # embs = [] # for angle in ANGLE_AUGS: # M = cv2.getRotationMatrix2D(center, angle, 1.0) # rot = cv2.warpAffine(v, M, (w, h), borderMode=cv2.BORDER_REFLECT_101) # embs.append(_embed_rgb(rot)) # # Average rotations for THIS variant # v_emb = np.mean(np.stack(embs), axis=0) # v_emb = v_emb / (np.linalg.norm(v_emb) + 1e-10) # final_vectors.append(v_emb) # return final_vectors # # ============================== # # PREDICTION # # ============================== # COSINE_THRESHOLD = 0.62 # # COSINE_THRESHOLD = 0.67 # COSINE_WEIGHT = 0.85 # # HYBRID_THRESHOLD = 0.55 # HYBRID_THRESHOLD = 0.63 # TOP2_MARGIN = 0.005 # def _class_similarity(class_embs, vec): # sims = cosine_similarity(class_embs, vec.reshape(1, -1)).ravel() # return float(np.mean(np.sort(sims)[-2:])) # def predict_robust(vectors): # """ # Matches multiple TTA vectors and takes the BEST (MAX) similarity. # """ # best_identity = 'unknown' # best_cosine = 0 # best_hybrid = 0 # best_second = 0 # # Try each TTA variant # for vec in vectors: # sims = {c: _class_similarity(emb, vec) for c, emb in gallery.items()} # sorted_sims = sorted(sims.items(), key=lambda x: x[1], reverse=True) # current_class, current_cos = sorted_sims[0] # current_second = sorted_sims[1][1] if len(sorted_sims) > 1 else 0 # mean_vec = gallery[current_class].mean(axis=0) # dist = np.linalg.norm(vec - mean_vec) # shape, loc, scale = weibull_models[current_class] # evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) # current_hybrid = COSINE_WEIGHT * current_cos + (1 - COSINE_WEIGHT) * evt_prob # # If THIS variant is better than our previous best, update # if current_cos > best_cosine: # best_identity = current_class # best_cosine = current_cos # best_hybrid = current_hybrid # best_second = current_second # # LOGGING FOR DEBUGGING # print(f"\n--- TTA Max-Score Debug ---") # print(f"Final Predicted: {best_identity}") # print(f"Max Cosine Score: {best_cosine:.4f} (Threshold: {COSINE_THRESHOLD})") # print(f"Hybrid Score: {best_hybrid:.4f} (Threshold: {HYBRID_THRESHOLD})") # print(f"---------------------------\n") # if best_cosine < COSINE_THRESHOLD: # return 'unknown', best_cosine, 0 # if best_cosine - best_second < TOP2_MARGIN: # return 'unknown', best_cosine, 0 # if best_hybrid < HYBRID_THRESHOLD: # return 'unknown', best_cosine, best_hybrid # return best_identity, best_cosine, best_hybrid # # ============================== # # LOGIN SYSTEM # # ============================== # attempt_log = defaultdict(list) # def login(image_path, session_id='default', silent=False): # vec = embed_image(image_path) # if vec is None: # return {'status': 'error'} # identity, cos, hyb = predict_robust(vec) # if identity == 'unknown': # return {'status': 'denied', 'identity': None, 'score': cos} # return {'status': 'granted', 'identity': identity, 'score': hyb} # # ============================== # # REGISTRATION (UNCHANGED) # # ============================== # def _embed_for_registration(image_path): # img = cv2.imread(image_path) # if img is None: # return [] # pp = preprocess_iris(img) # if pp is None: # return [] # embs = [] # for v in augment_lighting_variants(pp): # emb = embed_array(v) # emb = (emb - mean_all) / (std_all + 1e-10) # emb = emb / (np.linalg.norm(emb) + 1e-10) # embs.append(emb) # return embs # # def register_person(person_label, image_paths, gallery_pkl_path=PKL_PATH, overwrite=False): # # global gallery, weibull_models # # new_embs = [] # # for p in image_paths: # # new_embs.extend(_embed_for_registration(p)) # # new_embs = np.asarray(new_embs) # # if person_label in gallery: # # gallery[person_label] = np.vstack([gallery[person_label], new_embs]) # # else: # # gallery[person_label] = new_embs # # # weibull_models[person_label] = (1, 0, 1) # # # FIXED — real Weibull fit on actual embedding distances # # mean_vec = gallery[person_label].mean(axis=0) # # dists = np.linalg.norm(gallery[person_label] - mean_vec, axis=1) # # if len(dists) >= 3: # # # Need at least 3 points to fit Weibull reliably # # tail_size = max(3, int(0.3 * len(dists))) # use top 30% of distances # # tail = np.sort(dists)[-tail_size:] # # shape, loc, scale = weibull_min.fit(tail, floc=0) # # weibull_models[person_label] = (shape, loc, scale) # # print(f"✅ Weibull fitted for {person_label}: shape={shape:.3f}, scale={scale:.3f}") # # else: # # # Fallback if somehow less than 3 embeddings # # weibull_models[person_label] = (1, 0, 1) # # print(f"⚠️ Not enough embeddings for Weibull fit, using fallback") # # with open(gallery_pkl_path, 'wb') as f: # # pickle.dump({ # # 'gallery': gallery, # # 'weibull_models': weibull_models, # # 'mean_all': mean_all, # # 'std_all': std_all # # }, f) # # # UPLOAD TO HUGGING FACE # # try: # # print(f"📤 Uploading updated gallery to Hugging Face...") # # api = HfApi() # # api.upload_file( # # path_or_fileobj=gallery_pkl_path, # # path_in_repo=HF_FILENAME, # # repo_id=HF_REPO_ID, # # repo_type="model" # # ) # # print("✅ Gallery updated on Hugging Face!") # # except Exception as e: # # print(f"❌ Failed to upload to Hugging Face: {e}") # # return {'status': 'success', 'identity': person_label} # def register_person(person_label, image_paths, gallery_pkl_path=PKL_PATH, overwrite=False): # global gallery, weibull_models # new_embs = [] # for p in image_paths: # new_embs.extend(_embed_for_registration(p)) # new_embs = np.asarray(new_embs) # if person_label in gallery: # gallery[person_label] = np.vstack([gallery[person_label], new_embs]) # else: # gallery[person_label] = new_embs # # FIXED — real Weibull fit on actual embedding distances # mean_vec = gallery[person_label].mean(axis=0) # dists = np.linalg.norm(gallery[person_label] - mean_vec, axis=1) # if len(dists) >= 3: # # Need at least 3 points to fit Weibull reliably # tail_size = max(3, int(0.3 * len(dists))) # tail = np.sort(dists)[-tail_size:] # shape, loc, scale = weibull_min.fit(tail, floc=0) # weibull_models[person_label] = (shape, loc, scale) # print(f"✅ Weibull fitted for {person_label}: shape={shape:.3f}, scale={scale:.3f}") # else: # # Fallback if somehow less than 3 embeddings # weibull_models[person_label] = (1, 0, 1) # print(f"⚠️ Not enough embeddings for Weibull fit, using fallback") # with open(gallery_pkl_path, 'wb') as f: # pickle.dump({ # 'gallery': gallery, # 'weibull_models': weibull_models, # 'mean_all': mean_all, # 'std_all': std_all # }, f) # # UPLOAD TO HUGGING FACE # try: # print(f"📤 Uploading updated gallery to Hugging Face...") # api = HfApi() # api.upload_file( # path_or_fileobj=gallery_pkl_path, # path_in_repo=HF_FILENAME, # repo_id=HF_REPO_ID, # repo_type="model" # ) # print("✅ Gallery updated on Hugging Face!") # except Exception as e: # print(f"❌ Failed to upload to Hugging Face: {e}") # return {'status': 'success', 'identity': person_label} # print("✅ iris_recognition ready") import os import cv2 import time import csv import pickle import numpy as np from collections import defaultdict from datetime import datetime from scipy.stats import weibull_min from sklearn.metrics.pairwise import cosine_similarity from tensorflow.keras.applications import ResNet50 from tensorflow.keras.applications.resnet import preprocess_input from huggingface_hub import hf_hub_download, HfApi # ============================== # MODEL LOAD # ============================== model = ResNet50(weights='imagenet', include_top=False, pooling='avg') os.makedirs('static/debug', exist_ok=True) # ============================== # PREPROCESSING # ============================== IMG_SIZE = (224, 224) def normalize_lighting(img): """ Standard illumination normalization to maintain gallery compatibility. """ if img is None: return None gray_mean = np.mean(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) gamma = np.log(128) / (np.log(gray_mean + 1e-5)) gamma = np.clip(gamma, 0.4, 2.5) lut = np.array([((i / 255.0) ** (1.0 / gamma)) * 255 for i in range(256)], dtype=np.uint8) img = cv2.LUT(img, lut) lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) l = clahe.apply(l) lab = cv2.merge((l, a, b)) img = cv2.cvtColor(lab, cv2.COLOR_Lab2BGR) return img def _sharpen(img): kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) return cv2.filter2D(img, -1, kernel) # def _high_contrast(img): # lab = cv2.cvtColor(img, cv2.COLOR_BGR2Lab) # l, a, b = cv2.split(lab) # # Match the registration limit (5.0) # clahe = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # l = clahe.apply(l) # return cv2.cvtColor(cv2.merge((l, a, b)), cv2.COLOR_Lab2BGR) def preprocess_iris(img): if img is None: return None # 1. INITIAL CROP (Middle 50%) h, w = img.shape[:2] img = img[h // 4: 3 * h // 4, w // 4: 3 * w // 4] cv2.imwrite('static/debug/1_initial_crop.png', img) if len(img.shape) == 2 or img.shape[2] == 1: img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) # 2. LIGHTING NORMALIZATION img = normalize_lighting(img) cv2.imwrite('static/debug/2_normalized.png', img) # 3. FINAL RESIZE img = cv2.resize(img, IMG_SIZE) cv2.imwrite('static/debug/3_final_input.png', img) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) return img # ============================== # EMBEDDING # ============================== # def augment_lighting_variants(img): # """ # Creates a diverse set of environmental variants for registration. # These are added to the gallery ONLY for new registrations. # """ # variants = [img] # # 1. Brightness variants (stronger range) # variants.append(np.clip(img.astype(np.float32) * 1.6, 0, 255).astype(np.uint8)) # variants.append(np.clip(img.astype(np.float32) * 0.4, 0, 255).astype(np.uint8)) # # 2. High Contrast (High CLAHE) # lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) # l, a, b = cv2.split(lab) # clahe_high = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) # l = clahe_high.apply(l) # lab = cv2.merge((l, a, b)) # variants.append(cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB)) # # 3. Sharpening (Added as an augmentation variant only) # kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) # sharpened = cv2.filter2D(img, -1, kernel) # variants.append(sharpened) # # 4. Blur variant (simulates slight out-of-focus) # variants.append(cv2.GaussianBlur(img, (3, 3), 0)) # # 5. Noise variant (simulates sensor noise) # noise = np.random.normal(0, 8, img.shape).astype(np.int16) # variants.append(np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8)) # return variants def augment_lighting_variants(img): """ Creates a diverse set of environmental variants for registration. These are added to the gallery ONLY for new registrations. """ variants = [] # Ensure debug directory exists os.makedirs('static/debug', exist_ok=True) # 0. Original preprocessed image variants.append(img) cv2.imwrite('static/debug/reg_0_original.png', cv2.cvtColor(img, cv2.COLOR_RGB2BGR)) # Change: Save original # 1. Brightness variants (stronger range) bright = np.clip(img.astype(np.float32) * 1.6, 0, 255).astype(np.uint8) variants.append(bright) cv2.imwrite('static/debug/reg_1_bright.png', cv2.cvtColor(bright, cv2.COLOR_RGB2BGR)) # Change: Save bright variant dark = np.clip(img.astype(np.float32) * 0.4, 0, 255).astype(np.uint8) variants.append(dark) cv2.imwrite('static/debug/reg_1_dark.png', cv2.cvtColor(dark, cv2.COLOR_RGB2BGR)) # Change: Save dark variant # 2. High Contrast (High CLAHE) lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) l, a, b = cv2.split(lab) clahe_high = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) l = clahe_high.apply(l) lab = cv2.merge((l, a, b)) hc = cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB) variants.append(hc) cv2.imwrite('static/debug/reg_2_high_contrast.png', cv2.cvtColor(hc, cv2.COLOR_RGB2BGR)) # Change: Save high contrast variant # 3. Sharpening (Added as an augmentation variant only) kernel = np.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]]) sharpened = cv2.filter2D(img, -1, kernel) variants.append(sharpened) cv2.imwrite('static/debug/reg_3_sharpened.png', cv2.cvtColor(sharpened, cv2.COLOR_RGB2BGR)) # Change: Save sharpened variant # 4. Blur variant (simulates slight out-of-focus) blurred = cv2.GaussianBlur(img, (3, 3), 0) variants.append(blurred) cv2.imwrite('static/debug/reg_4_blurred.png', cv2.cvtColor(blurred, cv2.COLOR_RGB2BGR)) # Change: Save blurred variant # 5. Noise variant (simulates sensor noise) noise = np.random.normal(0, 8, img.shape).astype(np.int16) noisy = np.clip(img.astype(np.int16) + noise, 0, 255).astype(np.uint8) variants.append(noisy) cv2.imwrite('static/debug/reg_5_noisy.png', cv2.cvtColor(noisy, cv2.COLOR_RGB2BGR)) # Change: Save noisy variant return variants def embed_array(img_rgb): arr = preprocess_input(np.expand_dims(img_rgb.astype(np.float32), axis=0)) return model.predict(arr, verbose=0).flatten() # ============================== # LOAD GALLERY (FROM HUGGING FACE) # ============================== HF_REPO_ID = "Omamaa12/iris-models" HF_FILENAME = "iris_gallery_robustt.pkl" PKL_PATH = os.path.join('models', HF_FILENAME) def sync_gallery_from_hf(): """Downloads the latest gallery from Hugging Face.""" print(f"⏳ Syncing gallery from Hugging Face ({HF_REPO_ID})...") try: # Download to the models folder downloaded_path = hf_hub_download( repo_id=HF_REPO_ID, filename=HF_FILENAME, repo_type="model", local_dir="models", local_dir_use_symlinks=False, force_download=True, ) print(f"✅ Gallery synced: {downloaded_path}") return downloaded_path except Exception as e: print(f"⚠️ HF Sync failed, using local fallback: {e}") return PKL_PATH # Sync on startup PKL_PATH = sync_gallery_from_hf() if os.path.exists(PKL_PATH): with open(PKL_PATH, 'rb') as f: data = pickle.load(f) gallery = data['gallery'] weibull_models = data['weibull_models'] mean_all = data['mean_all'] std_all = data['std_all'] print(f"✅ Gallery loaded - {len(gallery)} identities.") else: print("❌ Gallery file not found! Initializing empty.") gallery = {} weibull_models = {} mean_all = None # These should ideally be pre-set std_all = None # ============================== # EMBED IMAGE (LOGIN) # ============================== ANGLE_AUGS = (-12, -6, 0, 6, 12) def _embed_rgb(rgb_img): arr = preprocess_input(np.expand_dims(rgb_img.astype(np.float32), axis=0)) emb = model.predict(arr, verbose=0).flatten() emb = (emb - mean_all) / (std_all + 1e-10) # ← ADD THIS LINE BACK emb = emb / (np.linalg.norm(emb) + 1e-10) return emb # def embed_image(image_path): # """ # Extracts multiple embeddings (TTA variants). # Returns a list of vectors. # """ # img = cv2.imread(image_path) # if img is None: # return None # pp = preprocess_iris(img) # if pp is None: # return None # # TTA variants: Standard, Sharpened, High Contrast # s = _sharpen(pp) # hc = _high_contrast(pp) # cv2.imwrite('static/debug/tta_sharpened.png', s) # cv2.imwrite('static/debug/tta_high_contrast.png', hc) # tta_variants = [pp, s, hc] # h, w = pp.shape[:2] # center = (w // 2, h // 2) # final_vectors = [] # for v in tta_variants: # embs = [] # for angle in ANGLE_AUGS: # M = cv2.getRotationMatrix2D(center, angle, 1.0) # rot = cv2.warpAffine(v, M, (w, h), borderMode=cv2.BORDER_REFLECT_101) # embs.append(_embed_rgb(rot)) # # Average rotations for THIS variant # v_emb = np.mean(np.stack(embs), axis=0) # v_emb = v_emb / (np.linalg.norm(v_emb) + 1e-10) # final_vectors.append(v_emb) # return final_vectors def _high_contrast(img): # img is RGB (from preprocess_iris) lab = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2BGR), cv2.COLOR_BGR2Lab) l, a, b = cv2.split(lab) clahe = cv2.createCLAHE(clipLimit=5.0, tileGridSize=(8, 8)) l = clahe.apply(l) lab = cv2.merge((l, a, b)) return cv2.cvtColor(cv2.cvtColor(lab, cv2.COLOR_Lab2BGR), cv2.COLOR_BGR2RGB) def embed_image(image_path): """ Extracts multiple embeddings (TTA variants). Returns a list of vectors. """ img = cv2.imread(image_path) if img is None: return None pp = preprocess_iris(img) if pp is None: return None # TTA variants: now 6 to match registration gallery coverage s = _sharpen(pp) hc = _high_contrast(pp) bright = np.clip(pp.astype(np.float32) * 1.6, 0, 255).astype(np.uint8) dark = np.clip(pp.astype(np.float32) * 0.4, 0, 255).astype(np.uint8) blurred = cv2.GaussianBlur(pp, (3, 3), 0) cv2.imwrite('static/debug/tta_sharpened.png', s) cv2.imwrite('static/debug/tta_high_contrast.png', hc) # cv2.imwrite('static/debug/tta_bright.png', bright) # cv2.imwrite('static/debug/tta_dark.png', dark) # cv2.imwrite('static/debug/tta_blurred.png', blurred) tta_variants = [pp, bright, dark, hc, s, blurred] h, w = pp.shape[:2] center = (w // 2, h // 2) final_vectors = [] for v in tta_variants: embs = [] for angle in ANGLE_AUGS: M = cv2.getRotationMatrix2D(center, angle, 1.0) rot = cv2.warpAffine(v, M, (w, h), borderMode=cv2.BORDER_REFLECT_101) embs.append(_embed_rgb(rot)) # Average rotations for THIS variant v_emb = np.mean(np.stack(embs), axis=0) v_emb = v_emb / (np.linalg.norm(v_emb) + 1e-10) final_vectors.append(v_emb) return final_vectors # ============================== # PREDICTION # ============================== COSINE_THRESHOLD = 0.62 # COSINE_THRESHOLD = 0.67 COSINE_WEIGHT = 0.85 # HYBRID_THRESHOLD = 0.55 HYBRID_THRESHOLD = 0.63 TOP2_MARGIN = 0.005 def _class_similarity(class_embs, vec): sims = cosine_similarity(class_embs, vec.reshape(1, -1)).ravel() k = min(6, len(sims)) return float(np.mean(np.sort(sims)[-k:])) def predict_robust(vectors): """ Matches multiple TTA vectors and takes the BEST (MAX) similarity. """ best_identity = 'unknown' best_cosine = 0 best_hybrid = 0 best_second = 0 # Try each TTA variant for vec in vectors: sims = {c: _class_similarity(emb, vec) for c, emb in gallery.items()} sorted_sims = sorted(sims.items(), key=lambda x: x[1], reverse=True) current_class, current_cos = sorted_sims[0] current_second = sorted_sims[1][1] if len(sorted_sims) > 1 else 0 mean_vec = gallery[current_class].mean(axis=0) dist = np.linalg.norm(vec - mean_vec) shape, loc, scale = weibull_models[current_class] evt_prob = 1 - weibull_min.cdf(dist, shape, loc=loc, scale=scale) current_hybrid = COSINE_WEIGHT * current_cos + (1 - COSINE_WEIGHT) * evt_prob # If THIS variant is better than our previous best, update if current_cos > best_cosine: best_identity = current_class best_cosine = current_cos best_hybrid = current_hybrid best_second = current_second # LOGGING FOR DEBUGGING print(f"\n--- TTA Max-Score Debug ---") print(f"Final Predicted: {best_identity}") print(f"Max Cosine Score: {best_cosine:.4f} (Threshold: {COSINE_THRESHOLD})") print(f"Hybrid Score: {best_hybrid:.4f} (Threshold: {HYBRID_THRESHOLD})") print(f"---------------------------\n") if best_cosine < COSINE_THRESHOLD: return 'unknown', best_cosine, 0 if best_cosine - best_second < TOP2_MARGIN: return 'unknown', best_cosine, 0 if best_hybrid < HYBRID_THRESHOLD: return 'unknown', best_cosine, best_hybrid return best_identity, best_cosine, best_hybrid # ============================== # LOGIN SYSTEM # ============================== attempt_log = defaultdict(list) def login(image_path, session_id='default', silent=False): vec = embed_image(image_path) if vec is None: return {'status': 'error'} identity, cos, hyb = predict_robust(vec) if identity == 'unknown': return {'status': 'denied', 'identity': None, 'score': cos} return {'status': 'granted', 'identity': identity, 'score': hyb} # ============================== # REGISTRATION (UNCHANGED) # ============================== def _embed_for_registration(image_path): img = cv2.imread(image_path) if img is None: return [] pp = preprocess_iris(img) if pp is None: return [] embs = [] for v in augment_lighting_variants(pp): emb = embed_array(v) # Must match the notebook gallery pipeline exactly: z-score then L2 emb = (emb - mean_all) / (std_all + 1e-10) emb = emb / (np.linalg.norm(emb) + 1e-10) embs.append(emb) return embs # def register_person(person_label, image_paths, gallery_pkl_path=PKL_PATH, overwrite=False): # global gallery, weibull_models # new_embs = [] # for p in image_paths: # new_embs.extend(_embed_for_registration(p)) # new_embs = np.asarray(new_embs) # if person_label in gallery: # gallery[person_label] = np.vstack([gallery[person_label], new_embs]) # else: # gallery[person_label] = new_embs # # weibull_models[person_label] = (1, 0, 1) # # FIXED — real Weibull fit on actual embedding distances # mean_vec = gallery[person_label].mean(axis=0) # dists = np.linalg.norm(gallery[person_label] - mean_vec, axis=1) # if len(dists) >= 3: # # Need at least 3 points to fit Weibull reliably # tail_size = max(3, int(0.3 * len(dists))) # use top 30% of distances # tail = np.sort(dists)[-tail_size:] # shape, loc, scale = weibull_min.fit(tail, floc=0) # weibull_models[person_label] = (shape, loc, scale) # print(f"✅ Weibull fitted for {person_label}: shape={shape:.3f}, scale={scale:.3f}") # else: # # Fallback if somehow less than 3 embeddings # weibull_models[person_label] = (1, 0, 1) # print(f"⚠️ Not enough embeddings for Weibull fit, using fallback") # with open(gallery_pkl_path, 'wb') as f: # pickle.dump({ # 'gallery': gallery, # 'weibull_models': weibull_models, # 'mean_all': mean_all, # 'std_all': std_all # }, f) # # UPLOAD TO HUGGING FACE # try: # print(f"📤 Uploading updated gallery to Hugging Face...") # api = HfApi() # api.upload_file( # path_or_fileobj=gallery_pkl_path, # path_in_repo=HF_FILENAME, # repo_id=HF_REPO_ID, # repo_type="model" # ) # print("✅ Gallery updated on Hugging Face!") # except Exception as e: # print(f"❌ Failed to upload to Hugging Face: {e}") # return {'status': 'success', 'identity': person_label} def register_person(person_label, image_paths, gallery_pkl_path=PKL_PATH, overwrite=False): global gallery, weibull_models new_embs = [] for p in image_paths: new_embs.extend(_embed_for_registration(p)) new_embs = np.asarray(new_embs) if person_label in gallery: gallery[person_label] = np.vstack([gallery[person_label], new_embs]) else: gallery[person_label] = new_embs # FIXED — real Weibull fit on actual embedding distances mean_vec = gallery[person_label].mean(axis=0) dists = np.linalg.norm(gallery[person_label] - mean_vec, axis=1) if len(dists) >= 3: # Need at least 3 points to fit Weibull reliably tail_size = max(3, int(0.3 * len(dists))) tail = np.sort(dists)[-tail_size:] shape, loc, scale = weibull_min.fit(tail, floc=0) weibull_models[person_label] = (shape, loc, scale) print(f"✅ Weibull fitted for {person_label}: shape={shape:.3f}, scale={scale:.3f}") else: # Fallback if somehow less than 3 embeddings weibull_models[person_label] = (1, 0, 1) print(f"⚠️ Not enough embeddings for Weibull fit, using fallback") with open(gallery_pkl_path, 'wb') as f: pickle.dump({ 'gallery': gallery, 'weibull_models': weibull_models, 'mean_all': mean_all, 'std_all': std_all }, f) # UPLOAD TO HUGGING FACE try: print(f"📤 Uploading updated gallery to Hugging Face...") api = HfApi() api.upload_file( path_or_fileobj=gallery_pkl_path, path_in_repo=HF_FILENAME, repo_id=HF_REPO_ID, repo_type="model" ) print("✅ Gallery updated on Hugging Face!") except Exception as e: print(f"❌ Failed to upload to Hugging Face: {e}") return {'status': 'success', 'identity': person_label} print("✅ iris_recognition ready")