{"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,"sourceType":"competition"}],"dockerImageVersionId":31193,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# --- CELL 1: THE \"FIX EVERYTHING\" INSTALLER ---\n# 1. Force NumPy < 2 (Critical for hloc/pycolmap)\n# 2. Install LightGlue (Critical for ALIKED)\n# 3. Install Kornia/Transformers (Critical for the pipeline)\n!pip install \"numpy<2\" --force-reinstall git+https://github.com/cvg/LightGlue.git kornia pycolmap transformers h5py\n\n# Clone repositories if not already present\n!git clone --recursive https://github.com/cvg/Hierarchical-Localization/\n!git clone https://github.com/magicleap/SuperGluePretrainedNetwork.git\n\nprint(\"✅ Installation Complete.\")\nprint(\"⚠️ CRITICAL: You MUST restart the session now for the NumPy downgrade to apply.\")\nprint(\"Go to: 'Run' (or 'Session') -> 'Restart Session' (or 'Restart Kernel').\")","metadata":{"trusted":true,"_kg_hide-input":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import sys\nimport os\nimport torch\nimport torch.nn.functional as F\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom PIL import Image\nfrom tqdm import tqdm\nimport torchvision.transforms as T\nimport shutil\nimport warnings\n\n# --- 1. CONFIGURATION & SETUP ---\nwarnings.filterwarnings(\"ignore\")\n\n# Setup Paths\ncurrent_dir = Path.cwd()\nif 'Hierarchical-Localization' not in sys.path:\n    sys.path.append(str(current_dir / 'Hierarchical-Localization'))\nif 'SuperGluePretrainedNetwork' not in sys.path:\n    sys.path.append(str(current_dir / 'SuperGluePretrainedNetwork'))\n\n# Manual LightGlue import fix\ntry:\n    import lightglue\nexcept ImportError:\n    import site\n    sys.path.append(site.getsitepackages()[0])\n\nimport pycolmap\nfrom hloc import extract_features, match_features, reconstruction\nfrom hloc.utils.read_write_model import read_model\n\n# Kaggle Directories\nKAGGLE_INPUT_DIR = Path('/kaggle/input/image-matching-challenge-2025')\n# POINT TO TRAIN DIRECTORY TO RUN ALL TRAIN DATASETS\nTRAIN_TEST_DIR = KAGGLE_INPUT_DIR / 'train' \nOUTPUT_DIR = Path('/kaggle/working/outputs')\n\n# Clean Start\nif OUTPUT_DIR.exists(): shutil.rmtree(OUTPUT_DIR)\nOUTPUT_DIR.mkdir(exist_ok=True, parents=True)\n\n# Models\nCONF_EXTRACT = {\n    'model': {\n        'name': 'aliked',\n        'model_name': 'aliked-n16',\n        'max_num_keypoints': 4096,\n        'detection_threshold': 0.01,\n        'nms_radius': 2,\n    },\n    'output': 'feats-aliked',\n    'preprocessing': {'resize_max': 2048, 'grayscale': False},\n}\nCONF_MATCH = match_features.confs['aliked+lightglue']\n\n# --- 2. HELPER CLASSES & FUNCTIONS ---\n\ndef qvec2rotmat(qvec):\n    return np.array([\n        [1 - 2 * qvec[2]**2 - 2 * qvec[3]**2,\n         2 * qvec[1] * qvec[2] - 2 * qvec[0] * qvec[3],\n         2 * qvec[3] * qvec[1] + 2 * qvec[0] * qvec[2]],\n        [2 * qvec[1] * qvec[2] + 2 * qvec[0] * qvec[3],\n         1 - 2 * qvec[1]**2 - 2 * qvec[3]**2,\n         2 * qvec[2] * qvec[3] - 2 * qvec[0] * qvec[1]],\n        [2 * qvec[3] * qvec[1] - 2 * qvec[0] * qvec[2],\n         2 * qvec[2] * qvec[3] + 2 * qvec[0] * qvec[1],\n         1 - 2 * qvec[1]**2 - 2 * qvec[2]**2]])\n\nclass AdaptivePairSelector:\n    def __init__(self, device='cuda'):\n        self.device = device if torch.cuda.is_available() else 'cpu'\n        print(f\"   [Selector] Loading DINOv2 on {self.device}...\")\n        self.model = torch.hub.load('facebookresearch/dinov2', 'dinov2_vitl14').to(self.device).eval()\n        self.transform = T.Compose([\n            T.Resize((224, 224)), T.ToTensor(),\n            T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ])\n\n    def extract_feat(self, img):\n        img_t = self.transform(img).unsqueeze(0).to(self.device)\n        with torch.no_grad():\n            feat = self.model(img_t)\n        return F.normalize(feat, p=2, dim=1)\n\n    def correct_rotations(self, image_paths):\n        # Rotation Logic (Only run if > 10 images)\n        if len(image_paths) < 10: return\n        \n        # Consensus\n        ref_feats = []\n        limit = min(5, len(image_paths))\n        for p in image_paths[:limit]:\n            img = Image.open(p).convert('RGB')\n            ref_feats.append(self.extract_feat(img))\n        scene_center = torch.mean(torch.cat(ref_feats), dim=0, keepdim=True)\n        \n        count = 0\n        for p in tqdm(image_paths, desc=\"   [Selector] Checking Rotation\"):\n            img_org = Image.open(p).convert('RGB')\n            best_score, best_rot, best_img = -1, 0, img_org\n            \n            for rot in [0, 90, 180, 270]:\n                if rot == 0: i_rot = img_org\n                elif rot == 90: i_rot = img_org.rotate(90, expand=True)\n                elif rot == 180: i_rot = img_org.rotate(180, expand=True)\n                elif rot == 270: i_rot = img_org.rotate(270, expand=True)\n                \n                feat = self.extract_feat(i_rot)\n                score = torch.mm(feat, scene_center.t()).item()\n                if score > best_score:\n                    best_score, best_rot, best_img = score, rot, i_rot\n            \n            if best_rot != 0:\n                best_img.save(p) # Overwrite in temp dir\n                count += 1\n        print(f\"   [Selector] Fixed {count} rotated images.\")\n\n    def get_pairs(self, image_paths, dataset_name):\n        # Run Rotation Check first\n        self.correct_rotations(image_paths)\n        \n        num_images = len(image_paths)\n        \n        # LOGIC: Force Exhaustive for high accuracy if under 300 images\n        if num_images < 300:\n            print(f\"   [Selector] Using EXHAUSTIVE matching ({num_images} images)\")\n            pairs = []\n            for i in range(num_images):\n                for j in range(i + 1, num_images):\n                    pairs.append((image_paths[i], image_paths[j]))\n            return pairs\n\n        # Strategy 2: DINOv2 for > 300\n        print(f\"   [Selector] Using DINOv2 filtering ({num_images} images)\")\n        features = []\n        bs = 16\n        with torch.no_grad():\n            for i in range(0, len(image_paths), bs):\n                batch = []\n                for p in image_paths[i:i+bs]:\n                    img = Image.open(p).convert('RGB')\n                    batch.append(self.transform(img))\n                batch_t = torch.stack(batch).to(self.device)\n                feat = self.model(batch_t)\n                features.append(F.normalize(feat, p=2, dim=1).cpu())\n        \n        all_feats = torch.cat(features)\n        sim = torch.mm(all_feats, all_feats.t())\n        \n        pairs = []\n        for i in range(num_images):\n            scores = sim[i]\n            valid = torch.where(scores > 0.15)[0]\n            valid = valid[valid != i]\n            if len(valid) > 0:\n                topk = torch.topk(scores[valid], min(50, len(valid))).indices\n                real_idx = valid[topk].tolist()\n                for j in real_idx:\n                    if i < j: pairs.append((image_paths[i], image_paths[j]))\n                    elif j < i: pairs.append((image_paths[j], image_paths[i]))\n        return sorted(list(set(pairs)))\n\ndef process_dataset(original_dataset_path, output_dir, pair_selector):\n    dataset_name = original_dataset_path.name\n    print(f\"\\n🚀 Processing {dataset_name}...\")\n    \n    # 1. Setup Temp Directory (Writable)\n    temp_dir = Path(f'/kaggle/working/temp_{dataset_name}')\n    if temp_dir.exists(): shutil.rmtree(temp_dir)\n    shutil.copytree(original_dataset_path, temp_dir)\n    \n    images = sorted([p for p in temp_dir.glob('**/*') if p.suffix.lower() in {'.jpg', '.png', '.jpeg'}])\n    if len(images) < 3: return False\n\n    # 2. Setup Outputs\n    ds_out = output_dir / dataset_name\n    ds_out.mkdir(exist_ok=True)\n    pairs_path = ds_out / 'pairs.txt'\n    feats_path = ds_out / 'feats.h5'\n    matches_path = ds_out / 'matches.h5'\n    \n    # Clean zombies\n    if feats_path.exists(): \n        if feats_path.is_dir(): shutil.rmtree(feats_path)\n        else: feats_path.unlink()\n    if matches_path.exists():\n        if matches_path.is_dir(): shutil.rmtree(matches_path)\n        else: matches_path.unlink()\n    \n    # 3. Generate Pairs\n    # Pass paths to temp images\n    raw_pairs = pair_selector.get_pairs(images, dataset_name)\n    \n    with open(pairs_path, 'w') as f:\n        for p1, p2 in raw_pairs:\n            rel1 = p1.relative_to(temp_dir).as_posix()\n            rel2 = p2.relative_to(temp_dir).as_posix()\n            f.write(f\"{rel1} {rel2}\\n\")\n\n    # 4. Run HLOC\n    img_list = [p.relative_to(temp_dir).as_posix() for p in images]\n    \n    extract_features.main(CONF_EXTRACT, temp_dir, feature_path=feats_path, image_list=img_list)\n    match_features.main(CONF_MATCH, pairs=pairs_path, features=feats_path, matches=matches_path)\n    \n    model_path = ds_out / 'colmap'\n    model = reconstruction.main(model_path, temp_dir, pairs=pairs_path, features=feats_path, matches=matches_path, camera_mode='AUTO', verbose=False)\n    \n    # Cleanup to save space\n    if temp_dir.exists(): shutil.rmtree(temp_dir)\n    \n    if model:\n        print(f\"✅ {dataset_name}: Reconstruction successful.\")\n        return True\n    else:\n        print(f\"⚠️ {dataset_name}: Failed to reconstruct.\")\n        return False\n\ndef merge_and_save_csv():\n    print(\"\\n🛠️ STARTING MULTI-MODEL MERGE & SAVE...\")\n    submission_data = {}\n    solved_count = 0\n    \n    # Scan all outputs\n    for dataset_dir in OUTPUT_DIR.iterdir():\n        if not dataset_dir.is_dir(): continue\n        dataset_name = dataset_dir.name\n        \n        # Find ALL binary models (handling fragmentation)\n        model_files = list(dataset_dir.glob('**/images.bin'))\n        \n        if model_files:\n            print(f\"   📂 {dataset_name}: Found {len(model_files)} model fragments.\")\n            \n            for m_file in model_files:\n                try:\n                    # Safe Binary Read\n                    _, images, _ = read_model(m_file.parent, ext='.bin')\n                    for img_id, img in images.items():\n                        if (dataset_name, img.name) in submission_data: continue\n                        \n                        # Pose Math\n                        R_mat = qvec2rotmat(img.qvec)\n                        tvec = img.tvec\n                        \n                        submission_data[(dataset_name, img.name)] = (\n                            \";\".join(map(str, R_mat.flatten())),\n                            \";\".join(map(str, tvec.flatten()))\n                        )\n                        solved_count += 1\n                except: pass\n    \n    # Write CSV\n    sample_path = KAGGLE_INPUT_DIR / 'sample_submission.csv'\n    if sample_path.exists():\n        df = pd.read_csv(sample_path)\n        out_rot, out_trans = [], []\n        for idx, row in df.iterrows():\n            key = (row['dataset'], row['image'])\n            if key in submission_data:\n                r, t = submission_data[key]\n                out_rot.append(r)\n                out_trans.append(t)\n            else:\n                out_rot.append(\"1.0;0.0;0.0;0.0;1.0;0.0;0.0;0.0;1.0\")\n                out_trans.append(\"0.0;0.0;0.0\")\n        df['rotation_matrix'] = out_rot\n        df['translation_vector'] = out_trans\n        df.to_csv('submission.csv', index=False)\n        print(f\"🎉 FINAL SUBMISSION SAVED! Total Solved Images: {solved_count}\")\n    else:\n        print(\"No sample submission found (Dry Run). Saving raw matches.\")\n        # Optional: Save raw df if no sample exists\n        # pd.DataFrame(...).to_csv()\n\n# --- 3. MAIN EXECUTION ---\nif __name__ == \"__main__\":\n    print(f\"📂 Source Directory: {TRAIN_TEST_DIR}\")\n    \n    # Initialize DINO\n    dino = AdaptivePairSelector()\n    \n    # --- GET ALL DATASETS AUTOMATICALLY ---\n    all_datasets = sorted([d for d in TRAIN_TEST_DIR.iterdir() if d.is_dir()])\n    print(f\"🚀 Starting Sequential Execution on ALL {len(all_datasets)} datasets...\")\n    \n    for dataset_folder in all_datasets:\n        ds_name = dataset_folder.name\n        \n        # Skip 'stairs' because it takes forever and guarantees 0% score\n        if 'stairs' in ds_name:\n            print(f\"⏭️ Skipping {ds_name} (Known failure case/High computation)\")\n            continue\n            \n        try:\n            process_dataset(dataset_folder, OUTPUT_DIR, dino)\n        except Exception as e:\n            print(f\"❌ Critical Error on {ds_name}: {e}\")\n            \n    # Final Step: Merge and Save\n    merge_and_save_csv()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-11T17:23:23.126900Z","iopub.execute_input":"2025-12-11T17:23:23.127517Z","iopub.status.idle":"2025-12-12T03:12:51.079633Z","shell.execute_reply.started":"2025-12-11T17:23:23.127482Z","shell.execute_reply":"2025-12-12T03:12:51.078749Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# THE \"MULTI-MODEL MERGER\" FIX (train accuracy 5 MODEL)---\nimport os\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nimport sys\nimport matplotlib.pyplot as plt\nimport networkx as nx\nimport plotly.graph_objs as go\n\n# Setup HLOC path\ncurrent_dir = Path.cwd()\nhloc_path = current_dir / 'Hierarchical-Localization'\nif str(hloc_path) not in sys.path:\n    sys.path.append(str(hloc_path))\n\n# Import Pure Python Reader\nfrom hloc.utils.read_write_model import read_model\n\n# Config\nOUTPUT_DIR = Path('/kaggle/working/outputs')\nKAGGLE_INPUT_DIR = Path('/kaggle/input/image-matching-challenge-2025') \n\nprint(\"🛠️ STARTING MULTI-MODEL MERGING...\")\n\nsubmission_data = {}\ndebug_counts = {}\n\n# 1. LOOP THROUGH DATASETS\nfor dataset_dir in OUTPUT_DIR.iterdir():\n    if not dataset_dir.is_dir(): continue\n    dataset_name = dataset_dir.name\n    \n    print(f\"\\n📂 Scanning {dataset_name}...\")\n    \n    # 2. FIND ALL SUB-MODELS (0, 1, 2...)\n    # COLMAP saves splits as separate folders inside 'colmap'\n    # We look recursively for ANY file named 'images.bin'\n    model_files = list(dataset_dir.glob('**/images.bin'))\n    \n    if not model_files:\n        print(f\"   ⚠️ No models found for {dataset_name}\")\n        continue\n        \n    print(f\"   ✅ Found {len(model_files)} fragmented models! Merging...\")\n    \n    total_imgs_for_ds = 0\n    \n    # 3. MERGE THEM\n    for m_file in model_files:\n        try:\n            # Read the model\n            _, images, points3D = read_model(m_file.parent, ext='.bin')\n            \n            # Count images in this fragment\n            n_fragment = len(images)\n            if n_fragment == 0: continue\n            \n            print(f\"      - Merging fragment from '{m_file.parent.name}': {n_fragment} images\")\n            \n            # Extract poses\n            for img_id, img in images.items():\n                # If image already exists (from a larger model), skip it\n                # We prioritize the first model we see (usually the largest 0)\n                if (dataset_name, img.name) in submission_data:\n                    continue\n                \n                # Pose Math\n                qvec = img.qvec\n                tvec = img.tvec\n                w, x, y, z = qvec\n                R_mat = np.array([\n                    [1-2*y*y-2*z*z, 2*x*y-2*z*w, 2*x*z+2*y*w],\n                    [2*x*y+2*z*w, 1-2*x*x-2*z*z, 2*y*z-2*x*w],\n                    [2*x*z-2*y*w, 2*y*z+2*x*w, 1-2*x*x-2*y*y]\n                ])\n                \n                submission_data[(dataset_name, img.name)] = (\n                    \";\".join(map(str, R_mat.flatten())),\n                    \";\".join(map(str, tvec.flatten()))\n                )\n                total_imgs_for_ds += 1\n                \n        except Exception as e:\n            print(f\"      ❌ Error reading {m_file}: {e}\")\n            \n    debug_counts[dataset_name] = total_imgs_for_ds\n    print(f\"   👉 Total merged count for {dataset_name}: {total_imgs_for_ds}\")\n\n# --- 4. SAVE SUBMISSION CSV ---\nsample_path = KAGGLE_INPUT_DIR / 'sample_submission.csv'\nif sample_path.exists():\n    df = pd.read_csv(sample_path)\n    out_rot, out_trans = [], []\n    filled_count = 0\n    \n    for idx, row in df.iterrows():\n        key = (row['dataset'], row['image'])\n        if key in submission_data:\n            r, t = submission_data[key]\n            out_rot.append(r)\n            out_trans.append(t)\n            filled_count += 1\n        else:\n            # Outlier / Failed\n            out_rot.append(\"1.0;0.0;0.0;0.0;1.0;0.0;0.0;0.0;1.0\")\n            out_trans.append(\"0.0;0.0;0.0\")\n    \n    df['rotation_matrix'] = out_rot\n    df['translation_vector'] = out_trans\n    df.to_csv('submission.csv', index=False)\n    \n    print(\"\\n\" + \"=\"*40)\n    print(f\"🎉 FINAL RESULT: Solved {filled_count} images!\")\n    print(\"=\"*40)\n    \n    # Recalculate your specific score\n    for ds, count in debug_counts.items():\n        total_in_ds = len(df[df['dataset'] == ds])\n        if total_in_ds > 0:\n            rate = (count / total_in_ds) * 100\n            print(f\"Dataset: {ds}\")\n            print(f\"  Accuracy: {rate:.2f}% ( {count} / {total_in_ds} )\")\n            if rate > 60: print(\"  Verdict: ⭐ EXCELLENT\")\n            elif rate > 30: print(\"  Verdict: ✅ IMPROVED\")\n            else: print(\"  Verdict: ⚠️ STILL LOW\")\n            print(\"-\" * 20)\n\nelse:\n    print(\"No sample submission found.\")\n\n# --- 5. VISUALIZATION (ALL MERGED MODELS) ---\nif debug_counts:\n    print(\"\\nGeneratng Visualization for ALL datasets...\")\n    \n    # Loop through all datasets that have results\n    for ds_name in debug_counts.keys():\n        print(f\"\\nVisualizing {ds_name}...\")\n        search_dir = OUTPUT_DIR / ds_name\n        \n        # Find the largest sub-model for this dataset to plot\n        candidates = list(search_dir.glob('**/images.bin'))\n        # Sort by file size (rough proxy for model size)\n        candidates.sort(key=lambda x: x.stat().st_size, reverse=True)\n        \n        if candidates:\n            # We visualize the largest fragment\n            try:\n                _, _, points3D = read_model(candidates[0].parent, ext='.bin')\n                pts, cols = [], []\n                for p3d in points3D.values():\n                    pts.append(p3d.xyz)\n                    cols.append(p3d.rgb)\n                \n                if len(pts) > 0:\n                    pts = np.array(pts)\n                    cols = np.array(cols) / 255.0\n                    \n                    fig = go.Figure()\n                    fig.add_trace(go.Scatter3d(\n                        x=pts[:,0], y=pts[:,1], z=pts[:,2],\n                        mode='markers', marker=dict(size=1.5, color=cols),\n                        name='Scene'\n                    ))\n                    fig.update_layout(title=f\"Reconstruction: {ds_name}\", height=600, template='plotly_dark')\n                    fig.show()\n                else:\n                    print(f\"   ⚠️ Model found but has no 3D points.\")\n            except Exception as e:\n                print(f\"   ❌ Error visualizing {ds_name}: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-12T03:25:03.936623Z","iopub.execute_input":"2025-12-12T03:25:03.936922Z","iopub.status.idle":"2025-12-12T03:25:19.022742Z","shell.execute_reply.started":"2025-12-12T03:25:03.936893Z","shell.execute_reply":"2025-12-12T03:25:19.020752Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}