| import argparse |
| import csv |
| import os |
| import time |
| import sys |
| import numpy as np |
| import yaml |
|
|
| |
| sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) |
|
|
| from src.similarity import numpy_vectorized_cosine |
| from src.evaluation import compute_confidence |
|
|
| REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) |
| DEFAULT_CONFIG_PATH = os.path.join(REPO_ROOT, 'configs', 'inference_config.yaml') |
|
|
| def _load_embedder_class(): |
| |
| from src.embedding import FaceEmbedder |
| return FaceEmbedder |
|
|
| def load_inference_config(config_path): |
| defaults = { |
| 'model_name': 'Facenet', |
| 'threshold': 0.35, |
| 'confidence_k': 10.0, |
| 'score_is_distance': False, |
| } |
|
|
| if config_path is None or not os.path.exists(config_path): |
| return defaults |
|
|
| with open(config_path, 'r') as f: |
| loaded = yaml.safe_load(f) or {} |
|
|
| config = defaults.copy() |
| config.update(loaded) |
|
|
| config['threshold'] = float(config['threshold']) |
| config['confidence_k'] = float(config['confidence_k']) |
| config['score_is_distance'] = bool(config['score_is_distance']) |
| config['model_name'] = str(config['model_name']) |
| return config |
|
|
| def run_inference( |
| img1_path, |
| img2_path, |
| threshold=0.35, |
| model_name='Facenet', |
| confidence_k=10.0, |
| score_is_distance=False, |
| embedder=None, |
| ): |
| """ |
| Run the full inference pipeline for one pair of images. |
| Returns a dictionary with result details. |
| """ |
| start_total = time.perf_counter() |
| if embedder is None: |
| FaceEmbedder = _load_embedder_class() |
| embedder = FaceEmbedder(model_name=model_name) |
| |
| |
| start_emb = time.perf_counter() |
| emb1 = embedder.compute_embedding(img1_path) |
| emb2 = embedder.compute_embedding(img2_path) |
| latency_emb = time.perf_counter() - start_emb |
| |
| |
| start_scoring = time.perf_counter() |
| |
| score = numpy_vectorized_cosine(emb1.reshape(1, -1), emb2.reshape(1, -1))[0] |
| latency_scoring = time.perf_counter() - start_scoring |
| |
| |
| if score_is_distance: |
| is_same = score < threshold |
| else: |
| is_same = score >= threshold |
| decision = "SAME" if is_same else "DIFFERENT" |
| |
| |
| confidence = compute_confidence( |
| score, |
| threshold, |
| k=confidence_k, |
| score_is_distance=score_is_distance, |
| ) |
| |
| latency_total = time.perf_counter() - start_total |
| |
| return { |
| "img1": img1_path, |
| "img2": img2_path, |
| "similarity_score": round(float(score), 4), |
| "threshold": threshold, |
| "decision": decision, |
| "is_same": int(is_same), |
| "confidence": round(confidence, 4), |
| "model_name": model_name, |
| "latency_total_ms": round(latency_total * 1000, 2), |
| "latency_emb_ms": round(latency_emb * 1000, 2), |
| "latency_scoring_ms": round(latency_scoring * 1000, 2) |
| } |
|
|
| def _resolve_runtime_config(args): |
| config = load_inference_config(args.config) |
| if args.threshold is not None: |
| config['threshold'] = float(args.threshold) |
| if args.model_name is not None: |
| config['model_name'] = str(args.model_name) |
| return config |
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="FaceID Inference CLI (Milestone 4)") |
| parser.add_argument( |
| "--config", |
| type=str, |
| default=None, |
| help="Path to inference YAML config", |
| ) |
| parser.add_argument("--img1", type=str, help="Path to first face image") |
| parser.add_argument("--img2", type=str, help="Path to second face image") |
| parser.add_argument( |
| "--threshold", |
| type=float, |
| default=None, |
| help="Optional threshold override. Default: 0.35", |
| ) |
| parser.add_argument( |
| "--model-name", |
| type=str, |
| default=None, |
| help="Optional embedding model override.", |
| ) |
| parser.add_argument("--batch", type=str, help="Path to batch CSV file (with left_path, right_path)") |
| |
| args = parser.parse_args() |
| runtime_config = _resolve_runtime_config(args) |
| |
| FaceEmbedder = _load_embedder_class() |
| embedder = FaceEmbedder(model_name=runtime_config['model_name']) |
| |
| if args.batch: |
| if not os.path.exists(args.batch): |
| print(f"Error: Batch file {args.batch} not found.") |
| return |
| |
| print(f"{'Img1':<40} | {'Img2':<40} | {'Score':<8} | {'Decision':<10} | {'Conf':<6} | {'Lat(ms)':<8}") |
| print("-" * 130) |
| |
| with open(args.batch, 'r') as f: |
| reader = csv.DictReader(f) |
| for row in reader: |
| res = run_inference( |
| row['left_path'], |
| row['right_path'], |
| threshold=runtime_config['threshold'], |
| model_name=runtime_config['model_name'], |
| confidence_k=runtime_config.get('confidence_k', 10.0), |
| score_is_distance=runtime_config.get('score_is_distance', False), |
| embedder=embedder, |
| ) |
| print(f"{os.path.basename(res['img1']):<40} | {os.path.basename(res['img2']):<40} | {res['similarity_score']:<8.4f} | {res['decision']:<10} | {res['confidence']:<6.4f} | {res['latency_total_ms']:<8.2f}") |
| |
| elif args.img1 and args.img2: |
| res = run_inference( |
| args.img1, |
| args.img2, |
| threshold=runtime_config['threshold'], |
| model_name=runtime_config['model_name'], |
| confidence_k=runtime_config.get('confidence_k', 10.0), |
| score_is_distance=runtime_config.get('score_is_distance', False), |
| embedder=embedder, |
| ) |
| print("\n--- FaceID Inference Result ---") |
| print(f"Inputs: {res['img1']} vs {res['img2']}") |
| print(f"Score: {res['similarity_score']:.4f} (Threshold: {res['threshold']})") |
| print(f"Decision: {res['decision']}") |
| print(f"Confidence: {res['confidence']:.4f}") |
| print(f"Latency: Total: {res['latency_total_ms']}ms (Emb: {res['latency_emb_ms']}ms, Scoring: {res['latency_scoring_ms']}ms)") |
| print("-------------------------------\n") |
| else: |
| parser.print_help() |
|
|
| if __name__ == "__main__": |
| main() |
|
|