FaceID / scripts /run_evaluation.py
Ypeng12's picture
Fix libgl1 package name for Debian Trixie/Bookworm in Dockerfile
69def8e
Raw
History Blame Contribute Delete
5.97 kB
import argparse
import os
import yaml
import csv
import json
import numpy as np
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from src.embedding import FaceEmbedder
from src.similarity import numpy_vectorized_cosine
from src.evaluation import compute_metrics, get_confusion_matrix
from src.tracking import Tracker
def load_config(path):
with open(path, 'r') as f:
return yaml.safe_load(f)
def read_pairs(csv_path):
pairs = []
with open(csv_path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
pairs.append({
'left_path': row['left_path'],
'right_path': row['right_path'],
'label': int(row['label'])
})
return pairs
def main():
parser = argparse.ArgumentParser(description="Evaluate face verification pipeline.")
parser.add_argument("--config", type=str, required=True, help="Path to evaluation config")
args = parser.parse_args()
config = load_config(args.config)
os.makedirs(config.get("output_dir", "outputs/eval"), exist_ok=True)
# Validation checks
csv_path = config["pairs_file"]
if not os.path.exists(csv_path):
raise FileNotFoundError(f"Pairs file {csv_path} not found.")
pairs = read_pairs(csv_path)
if len(pairs) == 0:
raise ValueError("Pairs file is empty.")
labels = [p['label'] for p in pairs]
if set(labels) - {0, 1}:
raise ValueError("Labels must be strictly 0 or 1.")
print(f"Loaded {len(pairs)} pairs from {csv_path}")
# Extract unique embeddings
unique_paths = set()
for p in pairs:
unique_paths.add(p['left_path'])
unique_paths.add(p['right_path'])
unique_paths = sorted(unique_paths)
for p in unique_paths:
if not os.path.exists(p):
raise FileNotFoundError(f"Image not found: {p}")
print(f"Extracting embeddings for {len(unique_paths)} unique images...")
embedder = FaceEmbedder()
emb_matrix = embedder.batch_compute_embeddings(unique_paths, batch_size=64)
path_to_emb = {p: emb_matrix[i] for i, p in enumerate(unique_paths)}
# Compute similarity scores
left_embs = np.array([path_to_emb[p['left_path']] for p in pairs])
right_embs = np.array([path_to_emb[p['right_path']] for p in pairs])
print("Computing cosine similarity scores...")
scores = numpy_vectorized_cosine(left_embs, right_embs)
target_labels = np.array(labels)
if len(scores) != len(pairs):
raise ValueError(f"Score count ({len(scores)}) does not match pair count ({len(pairs)}).")
tracker = Tracker()
if config.get("is_sweep", False):
print("Running threshold sweep [0.0 to 1.0]...")
thresholds = np.linspace(0.0, 1.0, 101)
sweep_data = []
best_f1 = -1
best_th = 0
for th in thresholds:
m = compute_metrics(scores, target_labels, th, score_is_distance=False)
# Must handle NaN f1 cases cleanly if possible, here handled in evaluation logic
sweep_data.append({"threshold": th, "tpr": m["tpr"], "fpr": m["fpr"], "f1": m["f1"]})
if m["f1"] > best_f1:
best_f1 = m["f1"]
best_th = th
metrics = {"best_threshold": float(best_th), "best_f1": float(best_f1)}
print(f"Sweep complete. Best F1: {best_f1:.4f} at threshold {best_th:.4f}")
sweep_file = os.path.join(config["output_dir"], f"sweep_{config['run_name']}.json")
with open(sweep_file, 'w') as f:
json.dump(sweep_data, f, indent=2)
print(f"Sweep data saved to {sweep_file}")
selected_threshold_file = os.path.join(
config["output_dir"],
f"selected_threshold_{config['run_name']}.json",
)
with open(selected_threshold_file, 'w') as f:
json.dump(
{
"run_name": config['run_name'],
"best_threshold": float(best_th),
"best_f1": float(best_f1),
"score_is_distance": bool(config.get("score_is_distance", False)),
},
f,
indent=2,
)
print(f"Selected threshold saved to {selected_threshold_file}")
tracker.log_run(config['run_name'], args.config, config['data_version'], True, float(best_th), metrics, config['note'])
else:
th = config["threshold"]
print(f"Evaluating at chosen threshold {th}...")
metrics = compute_metrics(scores, target_labels, th, score_is_distance=False)
cm = get_confusion_matrix(scores, target_labels, th, score_is_distance=False)
print("Metrics:", metrics)
print("Confusion Matrix:", cm)
preds = (scores >= th).astype(int)
# Slices
false_positives = [
{"left": pairs[i]['left_path'], "right": pairs[i]['right_path'], "score": float(scores[i])}
for i in range(len(pairs)) if preds[i] == 1 and target_labels[i] == 0
]
false_negatives = [
{"left": pairs[i]['left_path'], "right": pairs[i]['right_path'], "score": float(scores[i])}
for i in range(len(pairs)) if preds[i] == 0 and target_labels[i] == 1
]
error_file = os.path.join(config["output_dir"], f"errors_{config['run_name']}.json")
with open(error_file, 'w') as f:
json.dump({"false_positives": false_positives, "false_negatives": false_negatives}, f, indent=2)
print(f"Error slices saved to {error_file}")
metrics['cm'] = cm
tracker.log_run(config['run_name'], args.config, config['data_version'], False, float(th), metrics, config['note'])
print("Run logged successfully.")
if __name__ == "__main__":
main()