iris-backend / iris_recognition.py
trretretret's picture
updated
8828469
Raw
History Blame Contribute Delete
90.7 kB
# # # # 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")