| 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) |
| |
| |
| 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}") |
| |
| |
| 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)} |
| |
| |
| 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) |
| |
| 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) |
| |
| |
| 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() |
|
|