{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.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":91498,"databundleVersionId":11655853,"sourceType":"competition"},{"sourceId":11924458,"sourceType":"datasetVersion","datasetId":7497068},{"sourceId":11924650,"sourceType":"datasetVersion","datasetId":7497202}],"dockerImageVersionId":30746,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport cv2\nimport os\nfrom pathlib import Path\nfrom tqdm.notebook import tqdm # For progress bars in notebooks\n\n# --- Configuration and Paths ---\n# In a Kaggle notebook, these paths are usually pre-defined\nKAGGLE_ROOT_DIR = Path(\"/kaggle\")\nINPUT_DIR = KAGGLE_ROOT_DIR / \"input\" / \"image-matching-challenge-2025\"\nOUTPUT_DIR = KAGGLE_ROOT_DIR / \"working\"\n\n# Create output directory if it doesn't exist\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\nprint(f\"Kaggle Root Dir: {KAGGLE_ROOT_DIR}\")\nprint(f\"Input Dir: {INPUT_DIR}\")\nprint(f\"Output Dir: {OUTPUT_DIR}\")\n\n# --- Load Metadata (Train Labels and Sample Submission for structure) ---\n\n# For training, you'll load train_labels.csv\ntry:\n    train_labels_path = INPUT_DIR / \"train_labels.csv\"\n    train_df = pd.read_csv(train_labels_path)\n    print(\"\\nTrain Labels DataFrame (first 5 rows):\")\n    print(train_df.head())\n    print(f\"Total training images: {len(train_df)}\")\nexcept FileNotFoundError:\n    print(f\"Warning: train_labels.csv not found at {train_labels_path}. This is expected if running on the test set for final submission.\")\n    train_df = None # Set to None if not found\n\n# For the test set, you'll mainly rely on the structure of the input\n# The sample_submission.csv provided during inferencing will give you the image_id, dataset, image structure.\n# During submission, Kaggle populates the 'input' folder with the actual test data.\n# For local testing, you might use sample_submission.csv from the public dataset.\ntry:\n    sample_submission_path = INPUT_DIR / \"sample_submission.csv\"\n    submission_df_template = pd.read_csv(sample_submission_path)\n    print(\"\\nSample Submission Template (first 5 rows):\")\n    print(submission_df_template.head())\n    print(f\"Sample submission rows: {len(submission_df_template)}\")\nexcept FileNotFoundError:\n    print(f\"Warning: sample_submission.csv not found at {sample_submission_path}. This is expected during local development if you haven't downloaded it.\")\n    submission_df_template = None # Set to None if not found\n\n\n# --- Helper Functions for Data Handling ---\n\ndef get_image_path(base_dir: Path, dataset_name: str, image_filename: str) -> Path:\n    \"\"\"Constructs the full path to an image.\"\"\"\n    return base_dir / dataset_name / image_filename\n\ndef load_image(image_path: Path):\n    \"\"\"Loads an image using OpenCV, handling potential errors.\"\"\"\n    if not image_path.exists():\n        print(f\"Error: Image not found at {image_path}\")\n        return None\n    img = cv2.imread(str(image_path))\n    if img is None:\n        print(f\"Error: Could not load image at {image_path}\")\n    return img\n\ndef parse_matrix_string(matrix_str: str) -> np.ndarray:\n    \"\"\"Parses a flattened matrix string into a 3x3 numpy array.\"\"\"\n    values = np.array([float(x) for x in matrix_str.split(';')])\n    if len(values) == 9:\n        return values.reshape(3, 3)\n    elif len(values) == 3: # For translation vector\n        return values.reshape(3, 1) # Column vector\n    else:\n        raise ValueError(f\"Unexpected matrix string length: {len(values)}\")\n\ndef matrix_to_string(matrix: np.ndarray) -> str:\n    \"\"\"Converts a numpy array (3x3 or 3x1) to a flattened string.\"\"\"\n    return \";\".join(map(str, matrix.flatten()))\n\n\n# --- Example Usage (Loading a few images and parsing labels) ---\n\n# Example for training data (if available)\nif train_df is not None:\n    print(\"\\n--- Example: Processing Training Data ---\")\n    # Let's iterate through unique datasets and scenes to see the structure\n    for dataset_name in train_df['dataset'].unique():\n        print(f\"\\nProcessing training dataset: {dataset_name}\")\n        dataset_df = train_df[train_df['dataset'] == dataset_name]\n\n        # Get the base directory for this dataset (e.g., /kaggle/input/image-matching-challenge-2025/train/dataset_name)\n        current_dataset_base_dir = INPUT_DIR / \"train\" / dataset_name\n\n        for scene_name in dataset_df['scene'].unique():\n            print(f\"  Scene: {scene_name}\")\n            scene_df = dataset_df[dataset_df['scene'] == scene_name]\n\n            # Process a few images from this scene\n            for idx, row in scene_df.head(2).iterrows(): # Just take first 2 for example\n                image_filename = row['image']\n                full_image_path = get_image_path(current_dataset_base_dir, \"\", image_filename) # \"\" because dataset_name is already in current_dataset_base_dir\n                print(f\"    Attempting to load image: {full_image_path}\")\n                img = load_image(full_image_path)\n                if img is not None:\n                    print(f\"      Image loaded: shape {img.shape}, dtype {img.dtype}\")\n\n                    # Parse ground truth pose\n                    rotation_matrix_gt = parse_matrix_string(row['rotation_matrix'])\n                    translation_vector_gt = parse_matrix_string(row['translation_vector'])\n                    print(f\"      GT Rotation:\\n{rotation_matrix_gt}\")\n                    print(f\"      GT Translation:\\n{translation_vector_gt.T}\") # Transpose for better printing\n\n                # Re-convert to string to check consistency\n                rot_str_back = matrix_to_string(rotation_matrix_gt)\n                trans_str_back = matrix_to_string(translation_vector_gt)\n                # print(f\"      Rot string back: {rot_str_back == row['rotation_matrix']}\")\n                # print(f\"      Trans string back: {trans_str_back == row['translation_vector']}\")\n\n\n# --- Placeholder for Test Data Processing Logic ---\n# When your notebook is run for scoring, the 'test' directory will be populated.\n# You will get a `sample_submission.csv` that contains the `image_id`, `dataset`, `image` for the test set.\n# Your task will be to fill in the `scene`, `rotation_matrix`, `translation_vector`.\n\nif submission_df_template is not None:\n    print(\"\\n--- Example: Preparing for Test Data Processing ---\")\n    # The actual test data will be in INPUT_DIR / \"test\"\n    test_base_dir = INPUT_DIR / \"test\"\n\n    # You'll iterate through the unique datasets present in the submission template\n    # For actual submission, this template is the list of images you need to predict for.\n    test_datasets = submission_df_template['dataset'].unique()\n\n    # Create an empty list to store results for your final submission DataFrame\n    results = []\n\n    print(f\"Will process {len(test_datasets)} test datasets.\")\n\n    # In your actual solution, you'd loop through these and apply your full pipeline\n    for dataset_name in tqdm(test_datasets, desc=\"Processing Test Datasets\"):\n        print(f\"\\nProcessing test dataset: {dataset_name}\")\n        current_dataset_df = submission_df_template[submission_df_template['dataset'] == dataset_name].copy()\n\n        # You'll use this dataframe to get the images to process for this dataset.\n        # This will contain 'image_id', 'dataset', 'image'\n        # The 'scene', 'rotation_matrix', 'translation_vector' will be empty or placeholder.\n\n        # --- Your full pipeline for a given dataset would go here ---\n        # 1. Image retrieval / global features for this dataset to group images.\n        # 2. Iterate through groups (scenes) identified.\n        # 3. Perform local feature matching, geometric verification, SfM within each group.\n        # 4. Store results.\n\n        # Example placeholder: Just copy template structure for demonstration\n        # In a real scenario, you'd be replacing R, T, and scene labels with your predictions\n        for idx, row in current_dataset_df.iterrows():\n            image_id = row['image_id']\n            img_filename = row['image']\n            dataset_id = row['dataset']\n\n            # Placeholder values - YOU WILL REPLACE THESE\n            # For demonstration, let's just make up a dummy scene and identity pose\n            dummy_scene_id = f\"{dataset_id}_dummy_scene_001\" # You'll assign actual scene IDs\n            dummy_R = np.eye(3)\n            dummy_T = np.zeros((3, 1))\n\n            results.append({\n                'image_id': image_id,\n                'dataset': dataset_id,\n                'scene': dummy_scene_id,\n                'image': img_filename,\n                'rotation_matrix': matrix_to_string(dummy_R),\n                'translation_vector': matrix_to_string(dummy_T)\n            })\n\n    # Convert results to a DataFrame for submission\n    final_submission_df = pd.DataFrame(results)\n    final_submission_df_path = OUTPUT_DIR / \"submission.csv\"\n    final_submission_df.to_csv(final_submission_df_path, index=False)\n    print(f\"\\nDummy submission file created at: {final_submission_df_path}\")\n    print(final_submission_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:17:38.116275Z","iopub.execute_input":"2025-12-24T14:17:38.116647Z","iopub.status.idle":"2025-12-24T14:17:44.514634Z","shell.execute_reply.started":"2025-12-24T14:17:38.116616Z","shell.execute_reply":"2025-12-24T14:17:44.513596Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **Image Matching Algorithm**","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport cv2 # OpenCV is pre-installed\nimport os\nfrom pathlib import Path\nfrom tqdm.notebook import tqdm\nimport torch # This will now work correctly\nfrom torchvision import models, transforms\nfrom PIL import Image\nimport networkx as nx\nfrom sklearn.neighbors import NearestNeighbors\nfrom sklearn.metrics.pairwise import euclidean_distances # For diagnostic prints\n\n\n# --- Configuration and Paths ---\nKAGGLE_ROOT_DIR = Path(\"/kaggle\")\nINPUT_DIR = KAGGLE_ROOT_DIR / \"input\" / \"image-matching-challenge-2025\"\nOUTPUT_DIR = KAGGLE_ROOT_DIR / \"working\"\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# --- Define paths to your uploaded model weights ---\nMODEL_WEIGHTS_DIR = KAGGLE_ROOT_DIR / \"input\" / \"imc-2025-model-weights\" # Your dataset name\n\nRESNET18_WEIGHTS_PATH = MODEL_WEIGHTS_DIR / \"resnet18-f37072fd.pth\"\n\n\nprint(f\"\\n--- Checking Model Weights Setup ---\")\nprint(f\"Expected model weights directory: {MODEL_WEIGHTS_DIR}\")\n\nif not MODEL_WEIGHTS_DIR.exists():\n    raise FileNotFoundError(f\"ERROR: Model weights dataset directory '{MODEL_WEIGHTS_DIR}' not found. \"\n                            \"Please ensure this path is correct and the dataset is added.\")\n\nif not RESNET18_WEIGHTS_PATH.exists():\n    raise FileNotFoundError(f\"ERROR: ResNet18 weights not found at {RESNET18_WEIGHTS_PATH}. \"\n                            \"Please ensure 'resnet18-f37072fd.pth' is in your uploaded dataset.\")\nelse:\n    print(f\"SUCCESS: ResNet18 weights found at {RESNET18_WEIGHTS_PATH}\")\n\n\n# COLMAP installation block is REMOVED\n\n\n# --- Helper Functions ---\ndef get_image_path(base_dir: Path, dataset_name: str, image_filename: str) -> Path:\n    \"\"\"\n    Constructs the full path to an image, robust to case sensitivity on Linux.\n    It checks the exact path first. If not found, it tries to find a case-insensitive match.\n    \"\"\"\n    expected_path = base_dir / dataset_name / image_filename\n    if expected_path.exists():\n        return expected_path\n    \n    # If exact path not found, try to find a case-insensitive match\n    parent_dir = base_dir / dataset_name\n    if parent_dir.is_dir():\n        for actual_file_path in parent_dir.iterdir():\n            if actual_file_path.is_file() and actual_file_path.name.lower() == image_filename.lower():\n                return actual_file_path # Return the actual path with correct casing\n    \n    # If no match found, return the original expected path (which won't exist)\n    return expected_path\n\n\ndef load_image_pil(image_path: Path): # Used for image dimensions\n    if not image_path.exists():\n        print(f\"DEBUG: Image file NOT FOUND at {image_path}\") # NEW DEBUG\n        return None\n    try: \n        img = Image.open(image_path).convert(\"RGB\")\n        return img\n    except Exception as e: \n        print(f\"DEBUG: Error loading image {image_path}: {e}\") # NEW DEBUG\n        return None\ndef load_image_cv2(image_path: Path): # Used for OpenCV feature extraction\n    if not image_path.exists():\n        print(f\"DEBUG: Image file NOT FOUND (for CV2) at {image_path}\") # NEW DEBUG\n        return None\n    img = cv2.imread(str(image_path), cv2.IMREAD_GRAYSCALE) # Load as grayscale for SIFT/ORB\n    if img is None: print(f\"DEBUG: Could not load image (CV2 returned None) at {image_path}\"); return None\n    return img\ndef parse_matrix_string(matrix_str: str) -> np.ndarray:\n    values = np.array([float(x) for x in matrix_str.split(';')])\n    if len(values) == 9: return values.reshape(3, 3)\n    elif len(values) == 3: return values.reshape(3, 1)\n    else: raise ValueError(f\"Unexpected matrix string length: {len(values)}\")\ndef matrix_to_string(matrix: np.ndarray) -> str: return \";\".join(map(str, matrix.flatten()))\n\n\n# --- Load Metadata ---\ntrain_df = None; submission_df_template = None\ntry: train_labels_path = INPUT_DIR / \"train_labels.csv\"; train_df = pd.read_csv(train_labels_path); print(\"\\nTrain Labels DataFrame loaded.\")\nexcept FileNotFoundError: print(\"Warning: train_labels.csv not found. This is expected if running on test set.\")\ntry: sample_submission_path = INPUT_DIR / \"sample_submission.csv\"; submission_df_template = pd.read_csv(sample_submission_path); print(\"Sample Submission Template loaded.\")\nexcept FileNotFoundError: print(\"Warning: sample_submission.csv not found. This is expected during local development if not downloaded.\")\n\n\n# --- Global Descriptor Extraction (RESNET - ENABLED) ---\nclass GlobalFeatureExtractor:\n    def __init__(self, model_name='resnet18', device='cuda' if torch.cuda.is_available() else 'cpu', weights_path: Path = None):\n        self.device = device # Set to 'cuda' if available, 'cpu' otherwise\n        if model_name == 'resnet18':\n            self.model = models.resnet18(weights=None)\n            if weights_path and weights_path.exists():\n                self.model.load_state_dict(torch.load(weights_path, map_location=self.device))\n            else:\n                raise FileNotFoundError(f\"ERROR: ResNet18 weights not found at {weights_path}. Cannot proceed without them.\")\n            self.model = torch.nn.Sequential(*(list(self.model.children())[:-1]))\n        else: raise ValueError(f\"Model {model_name} not supported for global features.\")\n        self.model.eval(); self.model.to(self.device)\n        self.transform = transforms.Compose([\n            transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(),\n            transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n        ])\n    @torch.no_grad()\n    def extract(self, image_paths: list[Path]) -> tuple[np.ndarray, list[Path]]:\n        descriptors = []; valid_image_paths = []\n        print(f\"Extracting global features for {len(image_paths)} images...\")\n        for img_path in tqdm(image_paths, desc=\"Extracting global features\"):\n            pil_img = load_image_pil(img_path)\n            if pil_img is None: continue\n            input_tensor = self.transform(pil_img); input_batch = input_tensor.unsqueeze(0).to(self.device)\n            descriptor = self.model(input_batch).flatten().cpu().numpy()\n            descriptors.append(descriptor); valid_image_paths.append(img_path)\n        return np.array(descriptors), valid_image_paths\n\n# --- Global Feature Matching and Grouping (Using ResNet features) ---\ndef find_similar_images_and_group(image_descriptors: np.ndarray, all_image_paths: list[Path], n_neighbors: int = 50, distance_threshold: float = 0.5, min_images_per_scene: int = 5) -> dict[str, list[Path]]:\n    num_images = image_descriptors.shape[0]\n    if num_images == 0:\n        return {}\n\n    effective_n_neighbors = min(n_neighbors, num_images - 1)\n    \n    if effective_n_neighbors <= 0:\n        print(f\"Only {num_images} images. Cannot find {n_neighbors} neighbors. Each image will be its own scene.\")\n        scenes = {}\n        for i in range(num_images): scenes[f\"scene_{i:04d}\"] = [all_image_paths[i]]\n        return scenes\n\n    print(f\"Finding {effective_n_neighbors} nearest neighbors for {num_images} images using sklearn.neighbors...\")\n    neigh = NearestNeighbors(n_neighbors=effective_n_neighbors + 1, algorithm='brute', metric='euclidean'); neigh.fit(image_descriptors)\n    D, I = neigh.kneighbors(image_descriptors)\n\n    G = nx.Graph()\n    for i in range(num_images): G.add_node(i)\n    for i in tqdm(range(num_images), desc=\"Building similarity graph\"):\n        for j_idx in range(1, D.shape[1]): # Iterate over only the valid number of neighbors found for this image\n            neighbor_idx = I[i, j_idx]; distance = D[i, j_idx]\n            if distance < distance_threshold: G.add_edge(i, neighbor_idx, weight=distance)\n    print(f\"Graph built with {G.number_of_edges()} edges.\");\n    if G.number_of_edges() == 0 and num_images > 0:\n        print(\"No strong global matches found. Each image might be considered its own scene or an outlier.\")\n        scenes = {}\n        for i in range(num_images): scenes[f\"scene_{i:04d}\"] = [all_image_paths[i]]\n        return scenes\n    scenes = {}; scene_counter = 0; assigned_images = set()\n    print(\"Finding connected components for initial scene grouping...\")\n    for component_nodes in nx.connected_components(G):\n        if len(component_nodes) >= min_images_per_scene:\n            scene_id = f\"scene_{scene_counter:04d}\"; scenes[scene_id] = [all_image_paths[node] for node in component_nodes]; assigned_images.update(component_nodes); scene_counter += 1\n        else:\n            pass\n    unassigned_image_paths = [all_image_paths[i] for i in range(num_images) if i not in assigned_images]\n    if unassigned_image_paths:\n        print(f\"Identified {len(unassigned_image_paths)} potential outlier images (small components/isolated).\")\n        for img_path in unassigned_image_paths: scenes[f\"outlier_scene_{img_path.stem}\"] = [img_path]\n    print(f\"Identified {len(scenes)} scenes in total.\"); return scenes\n\n\n# --- OpenCV Local Feature Matching and Geometric Verification (AKAZE is a good choice here) ---\nclass OpenCVFeatureMatcher: # Renamed for clarity, using generic OpenCV features\n    def __init__(self, feature_type: str = 'AKAZE'): # Default to AKAZE for this pipeline\n        self.feature_type = feature_type.upper()\n        \n        if self.feature_type == 'SIFT':\n            self.detector = cv2.SIFT_create()\n            self.matcher = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True) # Use crossCheck=True for simple match\n        elif self.feature_type == 'ORB':\n            self.detector = cv2.ORB_create()\n            self.matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)\n        elif self.feature_type == 'AKAZE': # Using AKAZE for robustness\n            self.detector = cv2.AKAZE_create()\n            self.matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)\n        else:\n            raise ValueError(f\"Unsupported feature_type: {feature_type}. Choose from 'SIFT', 'ORB', 'AKAZE'.\")\n\n    def match_and_verify(self, img_path1: Path, img_path2: Path) -> tuple[np.ndarray, np.ndarray, np.ndarray]:\n        \"\"\"\n        Detects features, matches them, and performs geometric verification with RANSAC.\n        Returns:\n            - kp1_inliers (np.ndarray): Inlier keypoints from image 1 (N x 2)\n            - kp2_inliers (np.ndarray): Inlier keypoints from image 2 (N x 2)\n            - F (np.ndarray): The Fundamental Matrix\n        \"\"\"\n        img1 = load_image_cv2(img_path1)\n        img2 = load_image_cv2(img_path2)\n        if img1 is None or img2 is None: return np.array([]), np.array([]), None\n\n        # 1. Detect keypoints and compute descriptors\n        kp1, des1 = self.detector.detectAndCompute(img1, None)\n        kp2, des2 = self.detector.detectAndCompute(img2, None)\n\n        if des1 is None or des2 is None or len(kp1) < 8 or len(kp2) < 8:\n            return np.array([]), np.array([]), None\n\n        # 2. Match descriptors using simple match with crossCheck=True\n        matches = self.matcher.match(des1, des2)\n        \n        if len(matches) < 8: # Need at least 8 matches for RANSAC\n            return np.array([]), np.array([]), None\n\n        # Extract matched keypoints\n        src_pts = np.float32([ kp1[m.queryIdx].pt for m in matches ]).reshape(-1,2)\n        dst_pts = np.float32([ kp2[m.trainIdx].pt for m in matches ]).reshape(-1,2)\n\n        # 3. Find Fundamental Matrix using RANSAC\n        F, mask = cv2.findFundamentalMat(src_pts, dst_pts, cv2.FM_RANSAC, ransacReprojThreshold=1.0, confidence=0.999) # Standard RANSAC threshold\n\n        if F is None or mask is None:\n            return np.array([]), np.array([]), None\n\n        mask = mask.ravel() == 1 # Flatten mask to boolean array\n        kp1_inliers = src_pts[mask]\n        kp2_inliers = dst_pts[mask]\n\n        if len(kp1_inliers) < 8: # Re-check after RANSAC (set back to 8)\n            return np.array([]), np.array([]), None\n\n        return kp1_inliers, kp2_inliers, F\n\n    # Method to draw matches for visualization (for local debugging only) - REMOVED from main pipeline\n    # def draw_matches(...) { ... }\n\n\n# --- Main Processing Loop for Enhanced Score ---\n\nif submission_df_template is not None:\n    print(\"\\n--- Starting Enhanced (DL Global + OpenCV Local) Pipeline for Test Data Processing ---\")\n    test_base_dir = INPUT_DIR / \"test\"\n    test_datasets = submission_df_template['dataset'].unique()\n    all_results = []\n\n    # Initialize all_results by COPYING directly from submission_df_template.\n    # This preserves the original R/T values from the template.\n    all_results_list = submission_df_template.to_dict('records')\n    results_lookup = {item['image_id']: item for item in all_results_list}\n\n    # Initialize Global Feature Extractor (ResNet weights) and OpenCV Feature Matcher\n    device_to_use = 'cuda' if torch.cuda.is_available() else 'cpu'\n    global_feature_extractor = GlobalFeatureExtractor(weights_path=RESNET18_WEIGHTS_PATH, device=device_to_use)\n    # Use OpenCVFeatureMatcher here, not LocalFeatureMatcher (LoFTR)\n    opencv_feature_matcher = OpenCVFeatureMatcher(feature_type='AKAZE') # Switched back to AKAZE\n\n\n    for dataset_name in tqdm(test_datasets, desc=\"Overall Dataset Progress\"):\n        print(f\"\\nProcessing dataset: {dataset_name}\")\n        current_dataset_df = submission_df_template[submission_df_template['dataset'] == dataset_name].copy()\n        \n        image_filepaths = []\n        image_filename_to_original_id = {row['image']: row['image_id'] for idx, row in current_dataset_df.iterrows()}\n\n        for img_filename in image_filename_to_original_id.keys():\n            full_path = get_image_path(test_base_dir / dataset_name, \"\", img_filename)\n            image_filepaths.append(full_path)\n\n        if not image_filepaths:\n            print(f\"No images found for dataset {dataset_name}. Skipping.\")\n            # For these cases, results_lookup already has the original poses from sample_submission.csv\n            continue\n\n        # 1. Extract Global Descriptors and Group Images into Scenes (using ResNet)\n        descriptors, valid_image_paths = global_feature_extractor.extract(image_filepaths)\n\n        # --- DIAGNOSTIC PRINTS START ---\n        print(f\"\\nDEBUG INFO FOR DATASET: {dataset_name}\")\n        print(f\"DEBUG: Shape of descriptors: {descriptors.shape}\")\n        \n        if descriptors.shape[0] > 0:\n            print(f\"DEBUG: First 5 descriptors (first 5 values): {descriptors[:5, :5].tolist()}\")\n            print(f\"DEBUG: Is any descriptor NaN? {np.isnan(descriptors).any()}\")\n            print(f\"DEBUG: Is any descriptor Inf? {np.isinf(descriptors).any()}\")\n        else:\n            print(\"DEBUG: No descriptors extracted for this dataset.\")\n\n        if descriptors.shape[0] > 1: # Need at least 2 for distance calculations\n            sample_size = min(10, descriptors.shape[0])\n            sample_indices = np.random.choice(descriptors.shape[0], size=sample_size, replace=False) if descriptors.shape[0] > 10 else np.arange(descriptors.shape[0])\n            sample_descriptors = descriptors[sample_indices]\n            \n            distances_matrix = euclidean_distances(sample_descriptors, sample_descriptors)\n            \n            non_self_distances = distances_matrix[np.triu_indices(distances_matrix.shape[0], k=1)]\n            \n            if non_self_distances.size > 0:\n                print(f\"DEBUG: Min pairwise distance: {np.min(non_self_distances):.4f}\")\n                print(f\"DEBUG: Max pairwise distance: {np.max(non_self_distances):.4f}\")\n                print(f\"DEBUG: Median pairwise distance: {np.median(non_self_distances):.4f}\")\n                print(f\"DEBUG: Mean pairwise distance: {np.mean(non_self_distances):.4f}\")\n            else:\n                print(\"DEBUG: Not enough samples for meaningful pairwise calculation.\")\n        else:\n            print(\"DEBUG: Not enough descriptors to calculate pairwise distances.\")\n        # --- DIAGNOSTIC PRINTS END ---\n\n        grouped_scenes = find_similar_images_and_group(\n            descriptors,\n            valid_image_paths,\n            n_neighbors=50,\n            distance_threshold=20.0, # <--- TUNED BASED ON DEBUG OUTPUT (ETs dataset)\n            min_images_per_scene=2  # Min 2 images to attempt relative pose\n        )\n\n        # 2. Process each identified scene with Pairwise Pose Estimation (using OpenCV features)\n        for scene_id, scene_image_paths_raw in grouped_scenes.items():\n            print(f\"\\n  --- Processing Scene: {scene_id} ({len(scene_image_paths_raw)} images) ---\")\n\n            # Filter for images that actually exist on disk (robustness)\n            scene_image_paths = [p for p in scene_image_paths_raw if p.exists()]\n            if len(scene_image_paths) < 2:\n                print(f\"    Scene {scene_id} has only {len(scene_image_paths)} valid images. Skipping.\")\n                for img_path in scene_image_paths_raw:\n                    original_img_id = image_filename_to_original_id[img_path.name]\n                    # Only update scene ID, R/T remain from template\n                    results_lookup[original_img_id]['dataset'] = dataset_name\n                    results_lookup[original_img_id]['scene'] = scene_id\n                continue # Skip to next scene\n\n            # --- Pairwise Pose Estimation ---\n            first_img_pil = load_image_pil(scene_image_paths[0])\n            if first_img_pil is None:\n                print(f\"    Could not load first image for scene {scene_id}. Skipping.\")\n                for img_path in scene_image_paths_raw:\n                    original_img_id = image_filename_to_original_id[img_path.name]\n                    results_lookup[original_img_id]['dataset'] = dataset_name\n                    results_lookup[original_img_id]['scene'] = scene_id\n                continue\n\n            img_width, img_height = first_img_pil.size\n            \n            # Refined K: focal length proportional to max dim, principal point at center.\n            focal_length_factor = 1.0 # <--- TUNE THIS VALUE (1.0 is a good starting point for DL features)\n            focal_length = max(img_width, img_height) * focal_length_factor \n            K_default = np.array([\n                [focal_length, 0, img_width / 2],\n                [0, focal_length, img_height / 2],\n                [0, 0, 1]\n            ], dtype=np.float64)\n            print(f\"    Using assumed intrinsic matrix K:\\n{K_default}\")\n\n\n            # Assign pose for the reference image (first image in the scene)\n            ref_img_path = scene_image_paths[0]\n            ref_img_id = image_filename_to_original_id[ref_img_path.name]\n            \n            ref_R = np.eye(3)\n            ref_T = np.zeros((3, 1))\n\n            # Update existing entry in results_lookup\n            results_lookup[ref_img_id]['dataset'] = dataset_name\n            results_lookup[ref_img_id]['scene'] = scene_id\n            results_lookup[ref_img_id]['rotation_matrix'] = matrix_to_string(ref_R)\n            results_lookup[ref_img_id]['translation_vector'] = matrix_to_string(ref_T)\n            \n            # Process other images in the scene relative to the reference\n            for i in range(1, len(scene_image_paths)):\n                curr_img_path = scene_image_paths[i]\n                curr_img_id = image_filename_to_original_id[curr_img_path.name]\n\n                print(f\"    Estimating pose for {curr_img_path.name} relative to {ref_img_path.name}\")\n                kp1_inliers, kp2_inliers, F_matrix = opencv_feature_matcher.match_and_verify(ref_img_path, curr_img_path)\n\n                if F_matrix is not None and len(kp1_inliers) >= 8: # Need >= 8 points for E estimation\n                    try:\n                        E_matrix = K_default.T @ F_matrix @ K_default\n\n                        # 2. Decompose Essential Matrix to get R, T\n                        points1_norm = cv2.undistortPoints(kp1_inliers.reshape(-1, 1, 2), K_default, None)\n                        points2_norm = cv2.undistortPoints(kp2_inliers.reshape(-1, 1, 2), K_default, None)\n\n                        # recoverPose resolves the 4 possible (R,T) solutions and picks the best one\n                        _, R_relative, t_relative, _ = cv2.recoverPose(E_matrix, points1_norm, points2_norm, K_default)\n                        \n                        # R_relative and t_relative define the transformation from current camera to reference camera.\n                        # If reference is world origin, then R_w_c = R_relative, t_w_c = t_relative.\n\n                        # Update existing entry in results_lookup\n                        results_lookup[curr_img_id]['dataset'] = dataset_name\n                        results_lookup[curr_img_id]['scene'] = scene_id\n                        results_lookup[curr_img_id]['rotation_matrix'] = matrix_to_string(R_relative)\n                        results_lookup[curr_img_id]['translation_vector'] = matrix_to_string(t_relative)\n\n                    except Exception as e:\n                        print(f\"      Error recovering pose for {curr_img_path.name}: {e}. Keeping original pose.\")\n                        # If recovery fails, DO NOT OVERWRITE with dummy. Keep original from sample_submission.csv\n                        results_lookup[curr_img_id]['dataset'] = dataset_name\n                        results_lookup[curr_img_id]['scene'] = f\"{scene_id}_failed_pose_{curr_img_path.name}\"\n                        # R/T values remain as they were initialized from sample_submission.csv\n                else:\n                    print(f\"      Few SIFT inlier matches for {curr_img_path.name}. Keeping original pose.\")\n                    # If not enough matches, DO NOT OVERWRITE with dummy. Keep original from sample_submission.csv\n                    results_lookup[curr_img_id]['dataset'] = dataset_name\n                    results_lookup[curr_img_id]['scene'] = f\"{scene_id}_no_match_{curr_img_path.name}\"\n                    # R/T values remain as they were initialized from sample_submission.csv\n\n    # Convert the list of updated dicts back to DataFrame for final submission\n    final_submission_df = pd.DataFrame(all_results_list) # Use the initialized list\n    final_submission_df_path = OUTPUT_DIR / \"submission.csv\"\n    final_submission_df.to_csv(final_submission_df_path, index=False)\n    print(f\"\\nFinal submission file created at: {final_submission_df_path}\")\n    print(final_submission_df.head())\n    print(f\"Total rows in submission: {len(final_submission_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:17:44.517011Z","iopub.execute_input":"2025-12-24T14:17:44.517456Z","iopub.status.idle":"2025-12-24T14:18:17.414793Z","shell.execute_reply.started":"2025-12-24T14:17:44.517420Z","shell.execute_reply":"2025-12-24T14:18:17.413670Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef show_sample_images(base_dir, dataset_name, max_images=5):\n    dataset_dir = base_dir / dataset_name\n    if not dataset_dir.exists():\n        print(f\"Dataset folder not found: {dataset_dir}\")\n        return\n\n    image_files = list(dataset_dir.glob(\"*\"))[:max_images]\n\n    if not image_files:\n        print(\"No images found.\")\n        return\n\n    plt.figure(figsize=(15, 3))\n    for i, img_path in enumerate(image_files):\n        img = load_image_pil(img_path)\n        if img is None:\n            continue\n        plt.subplot(1, max_images, i + 1)\n        plt.imshow(img)\n        plt.axis(\"off\")\n        plt.title(img_path.name[:15])\n    plt.suptitle(f\"Sample Images from Dataset: {dataset_name}\")\n    plt.show()\n\n\n# --- CALL (example) ---\nexample_dataset = submission_df_template['dataset'].iloc[0]\nshow_sample_images(INPUT_DIR / \"test\", example_dataset)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:18:17.416669Z","iopub.execute_input":"2025-12-24T14:18:17.417072Z","iopub.status.idle":"2025-12-24T14:18:18.094381Z","shell.execute_reply.started":"2025-12-24T14:18:17.417036Z","shell.execute_reply":"2025-12-24T14:18:18.093428Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\n\nscene_names = []\nscene_sizes = []\n\nfor scene_id, info in scene_summary.items():\n    scene_names.append(scene_id)\n    scene_sizes.append(info[\"num_images\"])\n\nplt.figure(figsize=(12, 5))\nplt.bar(scene_names, scene_sizes)\nplt.xlabel(\"Scene ID\")\nplt.ylabel(\"Number of Images\")\nplt.title(\"Number of Images per Scene\")\nplt.xticks(rotation=90)\nplt.grid(axis=\"y\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:18:18.096537Z","iopub.execute_input":"2025-12-24T14:18:18.096829Z","iopub.status.idle":"2025-12-24T14:18:18.334007Z","shell.execute_reply.started":"2025-12-24T14:18:18.096805Z","shell.execute_reply":"2025-12-24T14:18:18.332999Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\nfrom collections import defaultdict\n\nprint(\"\\n--- Running Post-Processing & Quality Analysis ---\")\n\nscene_summary = defaultdict(lambda: {\n    \"num_images\": 0,\n    \"confidences\": [],\n    \"mean_confidence\": 0.0\n})\n\n# Read final submission\nsubmission_path = OUTPUT_DIR / \"submission.csv\"\nfinal_df = pd.read_csv(submission_path)\n\nfor _, row in final_df.iterrows():\n    scene_id = row['scene']\n    scene_summary[scene_id][\"num_images\"] += 1\n    \n    if 'confidence' in final_df.columns:\n        scene_summary[scene_id][\"confidences\"].append(\n            row.get(\"confidence\", 0)\n        )\n\n# Compute mean confidence\nfor scene_id, info in scene_summary.items():\n    if info[\"confidences\"]:\n        info[\"mean_confidence\"] = float(\n            np.mean(info[\"confidences\"])\n        )\n\nprint(f\"Total scenes detected: {len(scene_summary)}\")\n\n# Show top scenes\nsorted_scenes = sorted(\n    scene_summary.items(),\n    key=lambda x: (x[1][\"num_images\"], x[1][\"mean_confidence\"]),\n    reverse=True\n)\n\nprint(\"\\nTop 5 strongest scenes:\")\nfor s, info in sorted_scenes[:5]:\n    print(\n        f\"Scene: {s} | Images: {info['num_images']} | \"\n        f\"Mean Confidence: {info['mean_confidence']:.2f}\"\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:18:18.335367Z","iopub.execute_input":"2025-12-24T14:18:18.336013Z","iopub.status.idle":"2025-12-24T14:18:18.451250Z","shell.execute_reply.started":"2025-12-24T14:18:18.335977Z","shell.execute_reply":"2025-12-24T14:18:18.450216Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"scene_sizes = [info[\"num_images\"] for info in scene_summary.values()]\n\nplt.figure(figsize=(8, 4))\nplt.hist(scene_sizes, bins=20)\nplt.xlabel(\"Number of Images per Scene\")\nplt.ylabel(\"Frequency\")\nplt.title(\"Scene Size Distribution\")\nplt.grid(True)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:18:18.453969Z","iopub.execute_input":"2025-12-24T14:18:18.454518Z","iopub.status.idle":"2025-12-24T14:18:18.692893Z","shell.execute_reply.started":"2025-12-24T14:18:18.454492Z","shell.execute_reply":"2025-12-24T14:18:18.691886Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"scene_report_path = OUTPUT_DIR / \"scene_report.json\"\n\nwith open(scene_report_path, \"w\") as f:\n    json.dump(scene_summary, f, indent=4)\n\nprint(f\"\\nScene report saved to: {scene_report_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:18:18.694267Z","iopub.execute_input":"2025-12-24T14:18:18.694670Z","iopub.status.idle":"2025-12-24T14:18:18.701349Z","shell.execute_reply.started":"2025-12-24T14:18:18.694635Z","shell.execute_reply":"2025-12-24T14:18:18.700138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if 'confidence' in final_df.columns:\n    import matplotlib.pyplot as plt\n\n    confidences = final_df['confidence'].values\n\n    plt.figure(figsize=(8, 4))\n    plt.hist(confidences, bins=30)\n    plt.title(\"Pose Confidence Distribution (Inlier Count)\")\n    plt.xlabel(\"Inlier Matches\")\n    plt.ylabel(\"Frequency\")\n    plt.grid(True)\n    plt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:18:18.702492Z","iopub.execute_input":"2025-12-24T14:18:18.702758Z","iopub.status.idle":"2025-12-24T14:18:18.714756Z","shell.execute_reply.started":"2025-12-24T14:18:18.702736Z","shell.execute_reply":"2025-12-24T14:18:18.713635Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def sanity_check_submission(df):\n    assert 'rotation_matrix' in df.columns\n    assert 'translation_vector' in df.columns\n    \n    for i, row in df.head(10).iterrows():\n        R = parse_matrix_string(row['rotation_matrix'])\n        T = parse_matrix_string(row['translation_vector'])\n        \n        assert R.shape == (3, 3)\n        assert T.shape == (3, 1)\n\n    print(\"Sanity check PASSED ✔\")\n\nsanity_check_submission(final_df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:18:18.716012Z","iopub.execute_input":"2025-12-24T14:18:18.716483Z","iopub.status.idle":"2025-12-24T14:18:18.727603Z","shell.execute_reply.started":"2025-12-24T14:18:18.716449Z","shell.execute_reply":"2025-12-24T14:18:18.726676Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nPipeline completed successfully.\")\nprint(f\"Submission file : {submission_path}\")\nprint(f\"Scene report    : {scene_report_path}\")\nprint(f\"Total images    : {len(final_df)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-12-24T14:18:18.729705Z","iopub.execute_input":"2025-12-24T14:18:18.730020Z","iopub.status.idle":"2025-12-24T14:18:18.742786Z","shell.execute_reply.started":"2025-12-24T14:18:18.729986Z","shell.execute_reply":"2025-12-24T14:18:18.741857Z"}},"outputs":[],"execution_count":null}]}