{"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":"none","dataSources":[{"sourceId":126777,"databundleVersionId":15314950,"sourceType":"competition"},{"sourceId":297798014,"sourceType":"kernelVersion"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Jaguar LightGlue Similarity Search**","metadata":{}},{"cell_type":"markdown","source":"\n\n---\n\n# Experimental Result Summary\n\n**Feature Matching Analysis on Same Individual (Abril)**\n\nWe analyzed intra-class similarity for a single jaguar individual (\"Abril\") using **SuperPoint + LightGlue** feature matching.\n\n## Experimental Setup\n\n* Feature extractor: SuperPoint\n* Matcher: LightGlue\n* Reference image: `train_0001.png`\n* Compared against: 20 other images of the same individual\n* Similarity metric:\n  [\n  \\text{similarity} = \\frac{\\text{number of matches}}{100000}\n  ]\n\n---\n\n## Key Observations\n\n### 1. Extreme Variation in Match Counts\n\nEven though all images belong to the *same individual*, the number of matches varied dramatically:\n\n* Maximum: **2048 matches**\n* Minimum: **7 matches**\n* Over **two orders of magnitude difference**\n\nThis indicates that geometric feature matching is highly sensitive to:\n\n* Viewpoint changes\n* Body pose variations\n* Partial visibility (face vs torso)\n* Scale differences\n\n---\n\n### 2. Bimodal Distribution\n\nThe similarity distribution shows two distinct regimes:\n\n* **High-match cluster**\n  (300–2000 matches, high confidence)\n\n* **Low-match cluster**\n  (7–50 matches, low confidence)\n\nThis suggests that LightGlue performs well only when geometric structure is strongly preserved between images.\n\n---\n\n### 3. Confidence Correlates with Match Count\n\nAverage confidence decreases significantly as the number of matches decreases.\n\nThis implies:\n\n> When geometric consistency weakens, LightGlue becomes uncertain even for the same individual.\n\n---\n\n## Critical Insight\n\nAlthough all pairs represent the same jaguar, geometric matching frequently fails under viewpoint or pose variation.\n\nThis reveals a key limitation:\n\n> Feature matching methods optimized for geometric consistency are not inherently robust for identity recognition (Re-ID).\n\nJaguar Re-ID is fundamentally a **pattern recognition problem**, not a geometric alignment problem.\n\nLightGlue is designed to enforce spatial consistency, whereas Re-ID requires:\n\n* View-invariant pattern modeling\n* Texture-level discrimination\n* Global identity embeddings\n\n---\n\n## Conclusion\n\nThe experiment demonstrates that:\n\n* Intra-class similarity is not stable under geometric matching.\n* Match count alone is not a reliable identity metric.\n* LightGlue is likely not well-suited as a standalone solution for jaguar re-identification.\n\nFor this task, representation-learning approaches (e.g., CNN or Vision Transformer embeddings) are expected to be more appropriate.\n\n---\n\n","metadata":{}},{"cell_type":"code","source":"!pip install -q kornia kornia-rs\n!pip install -q git+https://github.com/cvg/LightGlue.git","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T04:02:35.819415Z","iopub.execute_input":"2026-02-15T04:02:35.819713Z","iopub.status.idle":"2026-02-15T04:04:18.676337Z","shell.execute_reply.started":"2026-02-15T04:02:35.819683Z","shell.execute_reply":"2026-02-15T04:04:18.675302Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport cv2\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm.auto import tqdm\nimport matplotlib.pyplot as plt\n\nfrom lightglue import LightGlue, SuperPoint\nfrom lightglue.utils import rbd\n\nprint(f\"PyTorch: {torch.__version__}\")\nprint(f\"CUDA available: {torch.cuda.is_available()}\")\nif torch.cuda.is_available():\n    print(f\"GPU: {torch.cuda.get_device_name(0)}\")\n    print(f\"GPU Memory: {torch.cuda.get_device_properties(0).total_memory / 1e9:.2f} GB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T04:04:18.679020Z","iopub.execute_input":"2026-02-15T04:04:18.679395Z","iopub.status.idle":"2026-02-15T04:04:29.899222Z","shell.execute_reply.started":"2026-02-15T04:04:18.679363Z","shell.execute_reply":"2026-02-15T04:04:29.898238Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"PAIRS_CSV = '/kaggle/input/jaguar-re-id/train.csv'       \nIMAGE_DIR = '/kaggle/input/notebooks/stpeteishii/jaguar-dataset-background-removal/train'      \nOUTPUT_CSV = 'submission.csv'  \nNORMALIZATION_FACTOR = 100000.0   ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T04:04:29.900156Z","iopub.execute_input":"2026-02-15T04:04:29.900690Z","iopub.status.idle":"2026-02-15T04:04:29.905693Z","shell.execute_reply.started":"2026-02-15T04:04:29.900661Z","shell.execute_reply":"2026-02-15T04:04:29.904815Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = 'cuda' if torch.cuda.is_available() else 'cpu'\nextractor = SuperPoint(max_num_keypoints=2048).eval().to(device)\nmatcher = LightGlue(features='superpoint').eval().to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T04:04:29.906614Z","iopub.execute_input":"2026-02-15T04:04:29.907038Z","iopub.status.idle":"2026-02-15T04:04:31.047596Z","shell.execute_reply.started":"2026-02-15T04:04:29.907013Z","shell.execute_reply":"2026-02-15T04:04:31.046709Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def compute_similarity(image1_path, image2_path, normalization_factor=100000.0):\n    \"\"\"\n    Computes the similarity between two images.\n    \n    Returns:\n        similarity (float): Similarity score\n        num_matches (int): Number of matching points\n        confidence (float): Average confidence\n    \"\"\"\n    # Load and preprocess images\n    img1 = cv2.imread(str(image1_path), cv2.IMREAD_GRAYSCALE)\n    img2 = cv2.imread(str(image2_path), cv2.IMREAD_GRAYSCALE)\n    \n    if img1 is None or img2 is None:\n        return 0.0, 0, 0.0\n    \n    # Convert to tensors\n    img1_tensor = torch.from_numpy(img1).float() / 255.0\n    img2_tensor = torch.from_numpy(img2).float() / 255.0\n    img1_tensor = img1_tensor.unsqueeze(0).unsqueeze(0).to(device)\n    img2_tensor = img2_tensor.unsqueeze(0).unsqueeze(0).to(device)\n    \n    # Feature extraction and matching\n    with torch.no_grad():\n        feats1 = extractor.extract(img1_tensor)\n        feats2 = extractor.extract(img2_tensor)\n        matches01 = matcher({'image0': feats1, 'image1': feats2})\n    \n    # Remove batch dimension\n    feats1, feats2, matches01 = [rbd(x) for x in [feats1, feats2, matches01]]\n    \n    # Get results\n    matches = matches01['matches']\n    scores = matches01['scores']\n    \n    # Count the number of matches\n    num_matches = (matches > -1).sum().item() // 2\n    confidence = scores.mean().item() if len(scores) > 0 else 0.0\n    similarity = num_matches / normalization_factor\n    \n    return similarity, num_matches, confidence","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T04:04:31.048582Z","iopub.execute_input":"2026-02-15T04:04:31.048926Z","iopub.status.idle":"2026-02-15T04:04:31.057859Z","shell.execute_reply.started":"2026-02-15T04:04:31.048898Z","shell.execute_reply":"2026-02-15T04:04:31.056774Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load CSV\ndf = pd.read_csv(PAIRS_CSV)\ndf = df[df['ground_truth']=='Abril']\nprint(f\"Total pairs: {len(df)}\")\ndisplay(df)\n\n# Test with the first pair\n#test_row = df.iloc[0]\ntest_img1 = Path(IMAGE_DIR) / 'train_0001.png'#test_row['query_image']\ntest_img2 = Path(IMAGE_DIR) / 'train_0002.png'#test_row['gallery_image']\n\nprint(f\"\\nRunning test: {test_img1.name} <-> {test_img2.name}\")\nsimilarity, num_matches, confidence = compute_similarity(test_img1, test_img2, NORMALIZATION_FACTOR)\n\nprint(f\"\\nResults:\")\nprint(f\"  Number of matches: {num_matches}\")\nprint(f\"  Similarity score: {similarity:.6f}\")\nprint(f\"  Average confidence: {confidence:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T04:04:31.059029Z","iopub.execute_input":"2026-02-15T04:04:31.059347Z","iopub.status.idle":"2026-02-15T04:04:42.427784Z","shell.execute_reply.started":"2026-02-15T04:04:31.059319Z","shell.execute_reply":"2026-02-15T04:04:42.426680Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Store results\nsimilarities = []\nnum_matches_list = []\nconfidences = []\nfiles=df['filename'].tolist()\n\n# Process with a progress bar\nfor idx, file in enumerate(files):\n    image1_path = Path(IMAGE_DIR) / 'train_0001.png'\n    image2_path = Path(IMAGE_DIR) / file\n    \n    try:\n        similarity, num_matches, confidence = compute_similarity(\n            image1_path, image2_path, NORMALIZATION_FACTOR\n        )\n        similarities.append(similarity)\n        num_matches_list.append(num_matches)\n        confidences.append(confidence)\n        \n    except Exception as e:\n        print(f\"\\nError at index {idx}: {e}\")\n        similarities.append(0.0)\n        num_matches_list.append(0)\n        confidences.append(0.0)\n\n# Add results to the DataFrame\ndf['similarity'] = similarities\ndf['num_matches'] = num_matches_list\ndf['confidence'] = confidences\n\ndisplay(df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-15T04:04:42.430695Z","iopub.execute_input":"2026-02-15T04:04:42.431126Z","execution_failed":"2026-02-15T04:06:32.712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nfig, axes = plt.subplots(2, 2, figsize=(15, 10))\n\naxes[0, 0].hist(similarities, bins=50, edgecolor='black')\naxes[0, 0].axvline(np.mean(similarities), color='red', linestyle='--', \n                   label=f'Mean: {np.mean(similarities):.6f}')\naxes[0, 0].set_xlabel('Similarity Score')\naxes[0, 0].set_ylabel('Frequency')\naxes[0, 0].set_title('Similarity Distribution')\naxes[0, 0].legend()\naxes[0, 0].grid(alpha=0.3)\n\naxes[0, 1].hist(num_matches_list, bins=50, edgecolor='black', color='green')\naxes[0, 1].axvline(np.mean(num_matches_list), color='red', linestyle='--',\n                   label=f'Mean: {np.mean(num_matches_list):.1f}')\naxes[0, 1].set_xlabel('Number of Matches')\naxes[0, 1].set_ylabel('Frequency')\naxes[0, 1].set_title('Match Count Distribution')\naxes[0, 1].legend()\naxes[0, 1].grid(alpha=0.3)\n\naxes[1, 0].scatter(num_matches_list, similarities, alpha=0.5)\naxes[1, 0].set_xlabel('Number of Matches')\naxes[1, 0].set_ylabel('Similarity Score')\naxes[1, 0].set_title('Matches vs Similarity')\naxes[1, 0].grid(alpha=0.3)\n\naxes[1, 1].hist(confidences, bins=50, edgecolor='black', color='orange')\naxes[1, 1].axvline(np.mean(confidences), color='red', linestyle='--',\n                   label=f'Mean: {np.mean(confidences):.4f}')\naxes[1, 1].set_xlabel('Match Confidence')\naxes[1, 1].set_ylabel('Frequency')\naxes[1, 1].set_title('Confidence Distribution')\naxes[1, 1].legend()\naxes[1, 1].grid(alpha=0.3)\n\nplt.tight_layout()\nplt.savefig('/content/similarity_analysis.png', dpi=300, bbox_inches='tight')\nplt.show()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-02-15T04:06:32.712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport torch\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport gc\nimport os\n\ndef visualize_matches(image1_path, image2_path, max_lines=200):\n    # --- 1. Memory Cleanup ---\n    plt.close('all')\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n\n    def process_background(path):\n        # Load image including alpha channel (BGRA)\n        img = cv2.imread(str(path), cv2.IMREAD_UNCHANGED)\n        if img is None:\n            return None, None\n        \n        # Check if the image has an alpha (transparency) channel\n        if img.shape[2] == 4:\n            b, g, r, a = cv2.split(img)\n            # Create a mask from the alpha channel (0 = transparent, 255 = opaque)\n            mask = a \n            # Merge BGR channels and fill transparent areas with pure black (0, 0, 0)\n            img_rgb = cv2.merge([b, g, r])\n            img_rgb[mask == 0] = 0 \n            # Convert to grayscale for feature extraction\n            img_gray = cv2.cvtColor(img_rgb, cv2.COLOR_BGR2GRAY)\n            return img_gray, img_rgb\n        else:\n            # If no alpha channel, proceed with standard conversion\n            img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n            # Ensure the visualization image is in BGR for consistency\n            img_vis = img if img.shape[2] == 3 else cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)\n            return img_gray, img_vis\n\n    # --- 2. Run Background Removal Process ---\n    img1_gray, img1_vis = process_background(image1_path)\n    img2_gray, img2_vis = process_background(image2_path)\n\n    if img1_gray is None or img2_gray is None:\n        print(\"Error: Could not load images.\")\n        return\n\n    # --- 3. Tensor Conversion ---\n    # Use .clone() to ensure fresh memory allocation\n    t1 = torch.from_numpy(img1_gray).float().clone().to(device) / 255.0\n    t2 = torch.from_numpy(img2_gray).float().clone().to(device) / 255.0\n    t1 = t1.unsqueeze(0).unsqueeze(0)\n    t2 = t2.unsqueeze(0).unsqueeze(0)\n\n    # --- 4. Matching (Black background ensures only the animal is targeted) ---\n    with torch.no_grad():\n        f1_raw = extractor.extract(t1)\n        f2_raw = extractor.extract(t2)\n        m01_raw = matcher({'image0': f1_raw, 'image1': f2_raw})\n\n    f1, f2, m01 = [rbd(x) for x in [f1_raw, f2_raw, m01_raw]]\n    kpts1, kpts2 = f1['keypoints'].cpu().numpy(), f2['keypoints'].cpu().numpy()\n    matches = m01['matches'].cpu().numpy()\n\n    valid = matches > -1\n    idx1, idx2 = np.where(valid)[0], matches[valid]\n\n    # Limit the number of lines for clarity\n    if len(idx1) > max_lines:\n        sel = np.random.choice(len(idx1), max_lines, replace=False)\n        idx1, idx2 = idx1[sel], idx2[sel]\n\n    # --- 5. Create Visualization Canvas ---\n    # Use the color images where the background has been physically removed (blacked out)\n    h1, w1 = img1_vis.shape[:2]\n    h2, w2 = img2_vis.shape[:2]\n    canvas = np.zeros((max(h1, h2), w1 + w2, 3), dtype=np.uint8)\n    canvas[:h1, :w1] = img1_vis\n    canvas[:h2, w1:] = img2_vis\n\n    # --- 6. Draw Matching Lines ---\n    for i1, i2 in zip(idx1, idx2):\n        pt1 = (int(kpts1[i1][0]), int(kpts1[i1][1]))\n        pt2 = (int(kpts2[i2][0]) + w1, int(kpts2[i2][1]))\n        # Generate random colors for each line\n        color = tuple(map(int, np.random.randint(0, 255, 3)))\n        cv2.line(canvas, pt1, pt2, color, 1, cv2.LINE_AA)\n\n    # --- 7. Display Results ---\n    plt.figure(figsize=(15, 8))\n    plt.imshow(canvas[:, :, ::-1]) # Convert BGR to RGB for matplotlib\n    plt.title(f\"Animal-only Matching: {len(idx1)} points\")\n    plt.axis(\"off\")\n    plt.show()\n\n    # Explicit memory cleanup for local variables\n    del img1_gray, img2_gray, canvas, t1, t2","metadata":{"trusted":true,"execution":{"execution_failed":"2026-02-15T04:06:32.712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"image1 = Path(IMAGE_DIR) / \"train_0001.png\"\nimage2 = Path(IMAGE_DIR) / \"train_0002.png\"  \nimage3 = Path(IMAGE_DIR) / \"train_0169.png\" \n\nimg1=plt.imread(image1)\nplt.imshow(img1)\nplt.show()\nimg2=plt.imread(image2)\nplt.imshow(img2)\nplt.show()\nimg3=plt.imread(image3)\nplt.imshow(img3)\nplt.show()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-02-15T04:06:32.712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_matches(image1, image2, max_lines=300)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-02-15T04:06:32.712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"visualize_matches(image1, image3, max_lines=300)","metadata":{"trusted":true,"execution":{"execution_failed":"2026-02-15T04:06:32.712Z"}},"outputs":[],"execution_count":null}]}