import sys import csv import pickle import logging from pathlib import Path import torch import numpy as np from PIL import Image from tqdm import tqdm sys.path.insert(0, str(Path(__file__).parents[1])) from backend.services.own_detector.model import load_model, TRANSFORM logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", datefmt="%H:%M:%S") logger = logging.getLogger(__name__) ROOT = Path(__file__).parents[1] MANIFEST = ROOT / "data" / "manifest.csv" OUTPUT_PATH = ROOT / "data" / "reference" / "own_centroids.pkl" SAMPLES_EACH = 5000 def extract_embeddings(rows, device, model): embeddings = [] for row in tqdm(rows, desc="Extracting embeddings"): img_path = ROOT / Path(row["path"]) try: img = Image.open(img_path).convert("RGB") tensor = TRANSFORM(img).unsqueeze(0).to(device) emb = model.extract_embedding(tensor) embeddings.append(emb.cpu().numpy().squeeze()) except Exception: continue return np.array(embeddings) def main(): device = "cuda" if torch.cuda.is_available() else "cpu" logger.info(f"Device: {device}") logger.info("Loading trained model") model = load_model(device) if model is None: logger.error("No trained model found. Run scripts/train_embedding.py first.") sys.exit(1) model.eval() logger.info("Reading manifest") real_rows, ai_rows = [], [] with open(MANIFEST, newline="", encoding="utf-8") as f: for row in csv.DictReader(f): p = ROOT / Path(row["path"]) if not p.exists(): continue if row["label"] == "real" and row["split"] == "train": real_rows.append(row) elif row["label"] == "ai" and row["split"] == "train": ai_rows.append(row) import random random.seed(42) random.shuffle(real_rows) random.shuffle(ai_rows) real_sample = real_rows[:SAMPLES_EACH] ai_sample = ai_rows[:SAMPLES_EACH] logger.info(f"Extracting real embeddings ({len(real_sample)} images)") real_embeddings = extract_embeddings(real_sample, device, model) logger.info(f"Extracting AI embeddings ({len(ai_sample)} images)") ai_embeddings = extract_embeddings(ai_sample, device, model) real_centroid = real_embeddings.mean(axis=0) ai_centroid = ai_embeddings.mean(axis=0) def cosine_sim(a, b): return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8)) separation = cosine_sim(real_centroid, ai_centroid) logger.info(f"Centroid cosine similarity (lower = better separation): {separation:.4f}") database = { "real_centroid": real_centroid.astype(np.float32), "ai_centroid": ai_centroid.astype(np.float32), "real_count": len(real_embeddings), "ai_count": len(ai_embeddings), "separation": separation, "model": "own_embedding_efficientnet_b0", "embedding_dim": real_centroid.shape[0], } OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True) with open(OUTPUT_PATH, "wb") as f: pickle.dump(database, f) logger.info(f"Saved centroids to {OUTPUT_PATH}") logger.info(f"Real centroid shape: {real_centroid.shape}") logger.info(f"Centroid separation: {separation:.4f}") if __name__ == "__main__": main()