{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91498,"databundleVersionId":11655853,"isSourceIdPinned":false,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"22k4125 Ibtesam,\n22k4039 Safey,\n22k8719 Shaheer","metadata":{}},{"cell_type":"code","source":"pip install kaggle kagglehub","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T16:52:15.806343Z","iopub.execute_input":"2025-12-08T16:52:15.806578Z","iopub.status.idle":"2025-12-08T16:52:22.014061Z","shell.execute_reply.started":"2025-12-08T16:52:15.806553Z","shell.execute_reply":"2025-12-08T16:52:22.012884Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import kagglehub\n\npath = kagglehub.competition_download(\"image-matching-challenge-2025\")\nprint(\"✅ Dataset downloaded at:\", path)\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-08T16:52:22.016537Z","iopub.execute_input":"2025-12-08T16:52:22.016833Z","iopub.status.idle":"2025-12-08T16:52:22.927086Z","shell.execute_reply.started":"2025-12-08T16:52:22.016799Z","shell.execute_reply":"2025-12-08T16:52:22.926227Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\n\n# Change this path to your competition data root after unzip\nbase_dir = kagglehub.competition_download(\"image-matching-challenge-2025\")\n\nprint(f\"📂 Listing all files under: {base_dir}\\n\")\n\nfor root, dirs, files in os.walk(base_dir):\n    level = root.replace(base_dir, \"\").count(os.sep)\n    indent = \" \" * 4 * level\n    print(f\"{indent}{os.path.basename(root)}/\")\n    subindent = \" \" * 4 * (level + 1)\n    for f in files:\n        print(f\"{subindent}{f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T16:52:22.928055Z","iopub.execute_input":"2025-12-08T16:52:22.928364Z","iopub.status.idle":"2025-12-08T16:52:27.584297Z","shell.execute_reply.started":"2025-12-08T16:52:22.928338Z","shell.execute_reply":"2025-12-08T16:52:27.583566Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport os\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import precision_score, recall_score, f1_score\n\n# === Paths ===\n# BASE_PATH = \"/kaggle/input/image-matching-challenge-2025\"\ntrain_dirs = [os.path.join(base_dir, \"train/ETs\"), os.path.join(base_dir, \"train/stairs\")]\ntest_dirs = [os.path.join(base_dir, \"test/ETs\"), os.path.join(base_dir, \"test/stairs\")]\n\n# === Helper functions ===\ndef load_images_from_folder(folder, max_images=20):\n    images = []\n    for fname in sorted(os.listdir(folder))[:max_images]:\n        if fname.lower().endswith(\".png\"):\n            path = os.path.join(folder, fname)\n            img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)\n            if img is not None:\n                images.append((fname, img))\n    return images\n\ndef match_images(img1, img2, sift, flann):\n    # Detect and compute features\n    kp1, des1 = sift.detectAndCompute(img1, None)\n    kp2, des2 = sift.detectAndCompute(img2, None)\n\n    if des1 is None or des2 is None:\n        return 0, 0, 0, []\n\n    # FLANN matching\n    matches = flann.knnMatch(des1, des2, k=2)\n    good = [m for m, n in matches if m.distance < 0.7 * n.distance]\n\n    return len(kp1), len(kp2), len(good), good\n\ndef evaluate_folder(train_imgs, test_imgs, sift, flann):\n    results = []\n    for tname, timg in test_imgs:\n        best_good = 0\n        best_train_name = None\n        for trname, trimg in train_imgs:\n            kp1, kp2, good_count, _ = match_images(trimg, timg, sift, flann)\n            if good_count > best_good:\n                best_good = good_count\n                best_train_name = trname\n        results.append((tname, best_train_name, best_good))\n    return results\n\n# === Initialize feature extractor & matcher ===\nsift = cv2.SIFT_create()\nFLANN_INDEX_KDTREE = 1\nflann = cv2.FlannBasedMatcher(dict(algorithm=FLANN_INDEX_KDTREE, trees=5), dict(checks=50))\n\n# === Process each category ===\nfinal_eval = {}\nfor category, (train_path, test_path) in zip([\"ETs\", \"stairs\"], zip(train_dirs, test_dirs)):\n    print(f\"\\n🔹 Processing category: {category}\")\n    train_imgs = load_images_from_folder(train_path)\n    test_imgs = load_images_from_folder(test_path)\n    \n    results = evaluate_folder(train_imgs, test_imgs, sift, flann)\n\n    # Aggregate results\n    total_good = np.mean([r[2] for r in results])\n    precision = np.mean([1 if r[2] > 10 else 0 for r in results])\n    recall = np.mean([1 if r[2] > 5 else 0 for r in results])\n    f1 = 2 * (precision * recall) / (precision + recall + 1e-8)\n\n    final_eval[category] = {\n        \"Avg Good Matches\": round(total_good, 2),\n        \"Precision\": round(precision, 2),\n        \"Recall\": round(recall, 2),\n        \"F1-Score\": round(f1, 2)\n    }\n\n    print(f\"  ✅ Avg Good Matches: {total_good:.2f}\")\n    print(f\"  ✅ Precision: {precision:.2f}\")\n    print(f\"  ✅ Recall: {recall:.2f}\")\n    print(f\"  ✅ F1-Score: {f1:.2f}\")\n\n# === Display summary ===\nimport pandas as pd\ndf = pd.DataFrame(final_eval).T\nprint(\"\\n📊 Final Evaluation Summary:\\n\")\ndisplay(df)\n\n# === Optional: visualize a sample match ===\nsample_train = train_imgs[0][1]\nsample_test = test_imgs[0][1]\nkp1, kp2, good_matches = None, None, None\n\nkp1, des1 = sift.detectAndCompute(sample_train, None)\nkp2, des2 = sift.detectAndCompute(sample_test, None)\nmatches = flann.knnMatch(des1, des2, k=2)\ngood = [m for m, n in matches if m.distance < 0.7 * n.distance]\nmatch_vis = cv2.drawMatches(sample_train, kp1, sample_test, kp2, good[:20], None, flags=2)\n\nplt.figure(figsize=(12,6))\nplt.imshow(match_vis)\nplt.title(\"Sample Keypoint Matches\")\nplt.axis(\"off\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T16:52:27.585111Z","iopub.execute_input":"2025-12-08T16:52:27.585463Z","iopub.status.idle":"2025-12-08T16:58:32.741671Z","shell.execute_reply.started":"2025-12-08T16:52:27.585441Z","shell.execute_reply":"2025-12-08T16:58:32.740837Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport os\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport pandas as pd\n\n# === Paths ===\n# base_dir = \"/kaggle/input/image-matching-challenge-2025\"  # Change as needed\n# output_dir = \"./output_matches\"\n# os.makedirs(output_dir, exist_ok=True)\n\ntrain_dirs = [os.path.join(base_dir, \"train/ETs\"), os.path.join(base_dir, \"train/stairs\")]\ntest_dirs = [os.path.join(base_dir, \"test/ETs\"), os.path.join(base_dir, \"test/stairs\")]\n\n# === Helper functions ===\ndef load_images_from_folder(folder, max_images=20):\n    images = []\n    for fname in sorted(os.listdir(folder))[:max_images]:\n        if fname.lower().endswith(\".png\"):\n            path = os.path.join(folder, fname)\n            img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)\n            if img is not None:\n                images.append((fname, img))\n    return images\n\ndef match_images(img1, img2, detector, matcher, ratio=0.7, use_knn=True):\n    # Detect keypoints and descriptors\n    kp1, des1 = detector.detectAndCompute(img1, None)\n    kp2, des2 = detector.detectAndCompute(img2, None)\n\n    if des1 is None or des2 is None or len(kp1) == 0 or len(kp2) == 0:\n        return kp1, kp2, 0, [], kp1, kp2\n\n    # Match descriptors\n    if use_knn:\n        matches = matcher.knnMatch(des1, des2, k=2)\n        good = []\n        for m,n in matches:\n            if m.distance < ratio * n.distance:\n                good.append(m)\n    else:\n        matches = matcher.match(des1, des2)\n        good = sorted(matches, key=lambda x: x.distance)\n\n    good_count = len(good)\n    return kp1, kp2, good_count, good, kp1, kp2\n\ndef evaluate_folder(train_imgs, test_imgs, detector, matcher, method_name):\n    results = []\n    for tname, timg in test_imgs:\n        best_good = 0\n        best_train_name = None\n        for trname, trimg in train_imgs:\n            kp1, kp2, good_count, _, _, _ = match_images(trimg, timg, detector, matcher)\n            if good_count > best_good:\n                best_good = good_count\n                best_train_name = trname\n        results.append((tname, best_train_name, best_good))\n    return results\n\ndef compute_metrics(results):\n    total_good = np.mean([r[2] for r in results])\n    precision = np.mean([1 if r[2] > 10 else 0 for r in results])\n    recall = np.mean([1 if r[2] > 5 else 0 for r in results])\n    f1 = 2 * (precision * recall) / (precision + recall + 1e-8)\n    return round(total_good, 2), round(precision, 2), round(recall, 2), round(f1, 2)\n\ndef visualize_match(train_imgs, test_imgs, detector, matcher, title, ratio=0.7, use_knn=True):\n    timg = test_imgs[0][1]\n    trimg = train_imgs[0][1]\n    kp1, kp2, good_count, good, kpA, kpB = match_images(trimg, timg, detector, matcher, ratio, use_knn)\n    vis = cv2.drawMatches(trimg, kpA, timg, kpB, good[:25], None, flags=2)\n    plt.figure(figsize=(12,6))\n    plt.imshow(vis, cmap='gray')\n    plt.title(f\"{title} | Good Matches: {good_count}\")\n    plt.axis(\"off\")\n    plt.show()\n\n\n# === Initialize detectors and matchers ===\n\n# SIFT + FLANN (float descriptors)\nsift = cv2.SIFT_create()\nFLANN_INDEX_KDTREE = 1\nflann_sift = cv2.FlannBasedMatcher(dict(algorithm=FLANN_INDEX_KDTREE, trees=5), dict(checks=50))\n\n# ORB + BFMatcher (binary descriptors)\norb = cv2.ORB_create(nfeatures=1000)\nbf_orb = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)\n\n# === Main Evaluation ===\nfinal_eval = []\n\nfor category, (train_path, test_path) in zip([\"ETs\", \"stairs\"], zip(train_dirs, test_dirs)):\n    print(f\"\\n🔹 Category: {category}\")\n    train_imgs = load_images_from_folder(train_path)\n    test_imgs = load_images_from_folder(test_path)\n\n    # --- SIFT + FLANN ---\n    sift_results = evaluate_folder(train_imgs, test_imgs, sift, flann_sift, \"SIFT\")\n    s_avg, s_prec, s_rec, s_f1 = compute_metrics(sift_results)\n\n    # --- ORB + BFMatcher ---\n    orb_results = evaluate_folder(train_imgs, test_imgs, orb, bf_orb, \"ORB\")\n    o_avg, o_prec, o_rec, o_f1 = compute_metrics(orb_results)\n\n    final_eval.append({\n        \"Category\": category,\n        \"SIFT AvgGood\": s_avg, \"SIFT Precision\": s_prec, \"SIFT Recall\": s_rec, \"SIFT F1\": s_f1,\n        \"ORB AvgGood\": o_avg, \"ORB Precision\": o_prec, \"ORB Recall\": o_rec, \"ORB F1\": o_f1\n    })\n\n# === Display summary ===\ndf = pd.DataFrame(final_eval)\nprint(\"\\n📊 Final Evaluation Summary:\\n\")\ndisplay(df)\n\n# === Visualize Matches ===\nfor category, (train_path, test_path) in zip([\"ETs\", \"stairs\"], zip(train_dirs, test_dirs)):\n    train_imgs = load_images_from_folder(train_path)\n    test_imgs = load_images_from_folder(test_path)\n    print(f\"\\n🖼 Visual Comparisons for {category}\")\n    visualize_match(train_imgs, test_imgs, sift, flann_sift, f\"SIFT + FLANN ({category})\")\n    visualize_match(train_imgs, test_imgs, orb, bf_orb, f\"ORB + BFMatcher ({category})\", use_knn=False)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T16:58:32.742538Z","iopub.execute_input":"2025-12-08T16:58:32.742946Z","iopub.status.idle":"2025-12-08T17:03:55.860358Z","shell.execute_reply.started":"2025-12-08T16:58:32.742924Z","shell.execute_reply":"2025-12-08T17:03:55.859533Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!git clone https://github.com/cvg/LightGlue.git","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T17:16:34.205013Z","iopub.execute_input":"2025-12-08T17:16:34.205555Z","iopub.status.idle":"2025-12-08T17:16:35.906540Z","shell.execute_reply.started":"2025-12-08T17:16:34.205528Z","shell.execute_reply":"2025-12-08T17:16:35.905585Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%cd LightGlue","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T17:16:39.826170Z","iopub.execute_input":"2025-12-08T17:16:39.826501Z","iopub.status.idle":"2025-12-08T17:16:39.835940Z","shell.execute_reply.started":"2025-12-08T17:16:39.826466Z","shell.execute_reply":"2025-12-08T17:16:39.834977Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python -m pip install -e .","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T17:16:41.948233Z","iopub.execute_input":"2025-12-08T17:16:41.948725Z","iopub.status.idle":"2025-12-08T17:18:31.246336Z","shell.execute_reply.started":"2025-12-08T17:16:41.948703Z","shell.execute_reply":"2025-12-08T17:18:31.245440Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 0️⃣ Setup — imports and device\n# ============================================================\nimport sys\nimport os\nimport torch\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\n\n# Device setup\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Running on: {device}\")\n\n# ============================================================\n# 1️⃣ Add LightGlue repo path from left panel\n# ============================================================\n# Suppose you attached LightGlue repo via left panel:\nLIGHTGLUE_PATH = \"/kaggle/working/LightGlue\"  # <-- update based on your input\nsys.path.append(LIGHTGLUE_PATH)\n\n# Now import LightGlue\nfrom lightglue import LightGlue, SuperPoint\n# from lightglue.utils import match_images","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T17:18:38.231796Z","iopub.execute_input":"2025-12-08T17:18:38.232085Z","iopub.status.idle":"2025-12-08T17:18:50.990703Z","shell.execute_reply.started":"2025-12-08T17:18:38.232055Z","shell.execute_reply":"2025-12-08T17:18:50.989754Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================================================\n# 2️⃣ Dataset paths\n# ============================================================\nBASE_PATH = \"/kaggle/input/image-matching-challenge-2025\"\nTRAIN_DIRS = [os.path.join(BASE_PATH, \"train/ETs\"),\n              os.path.join(BASE_PATH, \"train/stairs\")]\nTEST_DIRS  = [os.path.join(BASE_PATH, \"test/ETs\"),\n              os.path.join(BASE_PATH, \"test/stairs\")]\n\n# ============================================================\n# 3️⃣ Load images helper\n# ============================================================\ndef load_images(folder, max_images=10):\n    images = []\n    for fname in sorted(os.listdir(folder))[:max_images]:\n        if fname.lower().endswith(\".png\"):\n            path = os.path.join(folder, fname)\n            img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)\n            if img is not None:\n                images.append((fname, img))\n    return images\n\n# ============================================================\n# 4️⃣ Initialize models\n# ============================================================\nextractor = SuperPoint(max_num_keypoints=2048).eval().to(device)\nmatcher = LightGlue(features=\"superpoint\").eval().to(device)\n\n# ============================================================\n# 5️⃣ Compute LightGlue matches\n# ============================================================\ndef compute_lightglue_matches(img1, img2):\n    t1 = torch.from_numpy(img1)[None, None].float().to(device) / 255.\n    t2 = torch.from_numpy(img2)[None, None].float().to(device) / 255.\n\n    feats1 = extractor({\"image\": t1})\n    feats2 = extractor({\"image\": t2})\n\n    matches = matcher({\"image0\": feats1, \"image1\": feats2})\n    # number of valid matches\n    num_matches = (matches[\"matches0\"][0] >= 0).sum().item()\n    return num_matches, matches, feats1, feats2\n\n# ============================================================\n# 6️⃣ Evaluate category\n# ============================================================\ndef evaluate_category(train_path, test_path, name, max_images=10):\n    train_imgs = load_images(train_path, max_images)\n    test_imgs = load_images(test_path, max_images)\n    results = []\n\n    for tname, timg in tqdm(test_imgs, desc=f\"Processing {name}\"):\n        best_good = 0\n        best_train = None\n        for trname, trimg in train_imgs:\n            try:\n                good, _, _, _ = compute_lightglue_matches(trimg, timg)\n                if good > best_good:\n                    best_good = good\n                    best_train = trname\n            except Exception as e:\n                print(f\"⚠️ Error matching {trname} -> {tname}: {e}\")\n                continue\n        results.append((tname, best_train, best_good))\n\n    # Metrics\n    avg_matches = np.mean([r[2] for r in results])\n    precision = np.mean([1 if r[2] > 30 else 0 for r in results])\n    recall = np.mean([1 if r[2] > 15 else 0 for r in results])\n    f1 = 2 * (precision * recall) / (precision + recall + 1e-8)\n\n    summary = {\n        \"Category\": name,\n        \"Avg Matches\": float(round(avg_matches, 2)),\n        \"Precision\": float(round(precision, 2)),\n        \"Recall\": float(round(recall, 2)),\n        \"F1\": float(round(f1, 2))\n    }\n    return summary, results\n\nfor cat, (train_p, test_p) in zip([\"ETs\", \"stairs\"], zip(TRAIN_DIRS, TEST_DIRS)):\n    summary, _ = evaluate_category(train_p, test_p, cat, max_images=5)\n    \n    # Convert any NumPy scalars to Python scalars\n    summary_clean = {}\n    for k, v in summary.items():\n        if isinstance(v, np.ndarray):\n            summary_clean[k] = v.item() if v.size == 1 else str(v)\n        elif isinstance(v, np.generic):\n            summary_clean[k] = v.item()\n        else:\n            summary_clean[k] = v\n    \n    print(f\"\\n📊 LightGlue Evaluation Summary for {cat}:\")\n    for key, val in summary_clean.items():\n        print(f\"{key}: {val}\")\n\n\n\n\n\n\n# ============================================================\n# 8️⃣ Visualize matches\n# ============================================================\ndef visualize_lightglue_matches(img1, img2):\n    _, matches, feats1, feats2 = compute_lightglue_matches(img1, img2)\n\n    mkpts0 = feats1[\"keypoints\"][0][matches[\"matches0\"][0] >= 0].cpu().numpy()\n    mkpts1 = feats2[\"keypoints\"][0][matches[\"matches1\"][0] >= 0].cpu().numpy()\n\n    img1_color = cv2.cvtColor(img1, cv2.COLOR_GRAY2BGR)\n    img2_color = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)\n    concat = np.concatenate([img1_color, img2_color], axis=1)\n\n    for (x0, y0), (x1, y1) in zip(mkpts0, mkpts1):\n        cv2.circle(concat, (int(x0), int(y0)), 3, (0,255,0), -1)\n        cv2.circle(concat, (int(x1)+img1.shape[1], int(y1)), 3, (0,255,0), -1)\n        cv2.line(concat, (int(x0), int(y0)), (int(x1)+img1.shape[1], int(y1)), (255,0,0), 1)\n\n    plt.figure(figsize=(12,6))\n    plt.imshow(concat)\n    plt.axis(\"off\")\n    plt.show()\n\n\n# Helper to safely get first image from a folder\ndef get_first_image(folder):\n    imgs = load_images(folder, max_images=1)\n    if len(imgs) == 0:\n        print(f\"⚠️ No images found in {folder}\")\n        return None\n    return imgs[0][1]\n\n# Example visualizations\ntrain_img = get_first_image(TRAIN_DIRS[0])\ntest_img  = get_first_image(TEST_DIRS[0])\nif train_img is not None and test_img is not None:\n    print(\"🖼 Sample matches - ETs\")\n    visualize_lightglue_matches(train_img, test_img)\n\ntrain_img = get_first_image(TRAIN_DIRS[1])\ntest_img  = get_first_image(TEST_DIRS[1])\nif train_img is not None and test_img is not None:\n    print(\"🖼 Sample matches - Stairs\")\n    visualize_lightglue_matches(train_img, test_img)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T17:18:54.799563Z","iopub.execute_input":"2025-12-08T17:18:54.800074Z","iopub.status.idle":"2025-12-08T17:19:01.268494Z","shell.execute_reply.started":"2025-12-08T17:18:54.800046Z","shell.execute_reply":"2025-12-08T17:19:01.267773Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport cv2\nimport os\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom tqdm import tqdm\nfrom lightglue import LightGlue, SuperPoint\n\n# ============================================================\n# 1️⃣ Device\n# ============================================================\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"Running on: {device}\")\n\n# ============================================================\n# 2️⃣ Dataset paths\n# ============================================================\nBASE_PATH = \"/kaggle/input/image-matching-challenge-2025\"\nTRAIN_DIRS = [os.path.join(BASE_PATH, \"train/ETs\"), os.path.join(BASE_PATH, \"train/stairs\")]\nTEST_DIRS  = [os.path.join(BASE_PATH, \"test/ETs\"), os.path.join(BASE_PATH, \"test/stairs\")]\n\n# ============================================================\n# 3️⃣ Model initialization (SuperPoint + LightGlue)\n# ============================================================\nextractor = SuperPoint(max_num_keypoints=2048).eval().to(device)\nmatcher   = LightGlue(features=\"superpoint\").eval().to(device)\n\n# ============================================================\n# 4️⃣ Load images helper\n# ============================================================\ndef load_images(folder, max_images=10):\n    images = []\n    for fname in sorted(os.listdir(folder))[:max_images]:\n        if fname.lower().endswith(\".png\"):\n            img = cv2.imread(os.path.join(folder, fname), cv2.IMREAD_GRAYSCALE)\n            if img is not None:\n                images.append((fname, img))\n    return images\n\n# ============================================================\n# 5️⃣ Compute LightGlue matches\n# ============================================================\ndef compute_lightglue_matches(img1, img2):\n    t1 = torch.from_numpy(img1)[None, None].float().to(device) / 255.\n    t2 = torch.from_numpy(img2)[None, None].float().to(device) / 255.\n\n    feats1 = extractor({\"image\": t1})\n    feats2 = extractor({\"image\": t2})\n\n    matches = matcher({\"image0\": feats1, \"image1\": feats2})\n    num_matches = (matches[\"matches0\"][0] >= 0).sum().item()\n    return num_matches\n\n# ============================================================\n# 6️⃣ Evaluate a category\n# ============================================================\ndef evaluate_category(train_path, test_path, name, max_images=10):\n    train_imgs = load_images(train_path, max_images)\n    test_imgs  = load_images(test_path, max_images)\n\n    if len(train_imgs) == 0 or len(test_imgs) == 0:\n        print(f\"⚠️ No images found for {name}. Skipping...\")\n        return None, []\n\n    results = []\n    for tname, timg in tqdm(test_imgs, desc=f\"Processing {name}\"):\n        best_good = 0\n        best_train = None\n        for trname, trimg in train_imgs:\n            try:\n                good = compute_lightglue_matches(trimg, timg)\n                if good > best_good:\n                    best_good = good\n                    best_train = trname\n            except Exception as e:\n                continue\n        results.append((tname, best_train, best_good))\n\n    # Metrics\n    avg_matches = np.mean([r[2] for r in results])\n    precision   = np.mean([1 if r[2] > 30 else 0 for r in results])\n    recall      = np.mean([1 if r[2] > 15 else 0 for r in results])\n    f1          = 2 * (precision * recall) / (precision + recall + 1e-8)\n\n    summary = {\n        \"Category\": name,\n        \"Avg Matches\": round(avg_matches, 2),\n        \"Precision\": round(precision, 2),\n        \"Recall\": round(recall, 2),\n        \"F1\": round(f1, 2)\n    }\n    return summary, results\n\n# ============================================================\n# 7️⃣ Run evaluation and print summaries\n# ============================================================\nall_results = {}\nfor cat, (train_p, test_p) in zip([\"ETs\", \"stairs\"], zip(TRAIN_DIRS, TEST_DIRS)):\n    summary, results = evaluate_category(train_p, test_p, cat, max_images=5)\n    if summary is not None:\n        all_results[cat] = {\"summary\": summary, \"pairs\": results}\n        print(f\"\\n📊 LightGlue Evaluation Summary for {cat}:\")\n        for key, val in summary.items():\n            print(f\"{key}: {val}\")\n\n# ============================================================\n# 8️⃣ Visualization at the END\n# ============================================================\ndef visualize_lightglue_matches(img1, img2):\n    t1 = torch.from_numpy(img1)[None, None].float().to(device) / 255.\n    t2 = torch.from_numpy(img2)[None, None].float().to(device) / 255.\n\n    feats1 = extractor({\"image\": t1})\n    feats2 = extractor({\"image\": t2})\n\n    matches = matcher({\"image0\": feats1, \"image1\": feats2})\n\n    mkpts0 = feats1[\"keypoints\"][0][matches[\"matches0\"][0] >= 0].cpu().numpy()\n    mkpts1 = feats2[\"keypoints\"][0][matches[\"matches1\"][0] >= 0].cpu().numpy()\n\n    img1_color = cv2.cvtColor(img1, cv2.COLOR_GRAY2BGR)\n    img2_color = cv2.cvtColor(img2, cv2.COLOR_GRAY2BGR)\n    concat = np.concatenate([img1_color, img2_color], axis=1)\n\n    for (x0, y0), (x1, y1) in zip(mkpts0, mkpts1):\n        cv2.circle(concat, (int(x0), int(y0)), 3, (0,255,0), -1)\n        cv2.circle(concat, (int(x1)+img1.shape[1], int(y1)), 3, (0,255,0), -1)\n        cv2.line(concat, (int(x0), int(y0)), (int(x1)+img1.shape[1], int(y1)), (255,0,0), 1)\n\n    plt.figure(figsize=(12,6))\n    plt.imshow(concat)\n    plt.axis(\"off\")\n    plt.show()\n\n# Visualize first image pair for each category\nfor train_p, test_p, cat in zip(TRAIN_DIRS, TEST_DIRS, [\"ETs\", \"stairs\"]):\n    train_imgs = load_images(train_p, max_images=1)\n    test_imgs  = load_images(test_p, max_images=1)\n    if len(train_imgs) > 0 and len(test_imgs) > 0:\n        print(f\"🖼 Sample matches - {cat}\")\n        visualize_lightglue_matches(train_imgs[0][1], test_imgs[0][1])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-08T17:19:07.271612Z","iopub.execute_input":"2025-12-08T17:19:07.271927Z","iopub.status.idle":"2025-12-08T17:19:11.373976Z","shell.execute_reply.started":"2025-12-08T17:19:07.271903Z","shell.execute_reply":"2025-12-08T17:19:11.373197Z"}},"outputs":[],"execution_count":null}]}