FaceID / scripts /inference.py
Ypeng12's picture
Fix libgl1 package name for Debian Trixie/Bookworm in Dockerfile
69def8e
Raw
History Blame Contribute Delete
6.47 kB
import argparse
import csv
import os
import time
import sys
import numpy as np
import yaml
# Add workspace to path
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():
# Lazy import keeps CLI helper paths lightweight and test-friendly.
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)
# 1. Preprocessing & Embedding Extraction
start_emb = time.perf_counter()
emb1 = embedder.compute_embedding(img1_path)
emb2 = embedder.compute_embedding(img2_path)
latency_emb = time.perf_counter() - start_emb
# 2. Similarity Scoring
start_scoring = time.perf_counter()
# Reshape for vectorized function
score = numpy_vectorized_cosine(emb1.reshape(1, -1), emb2.reshape(1, -1))[0]
latency_scoring = time.perf_counter() - start_scoring
# 3. Decision
if score_is_distance:
is_same = score < threshold
else:
is_same = score >= threshold
decision = "SAME" if is_same else "DIFFERENT"
# 4. Calibrated Confidence
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()