{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceType":"competition","sourceId":91498,"databundleVersionId":11655853}],"dockerImageVersionId":31328,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport os\nimport seaborn as sns\nfrom matplotlib import pyplot as plt\n# import pycolmap\nimport cv2\nfrom PIL import Image\nimport networkx as nx\nfrom collections import defaultdict\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#     for filename in filenames:\n#         print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')\nprint(os.listdir('/kaggle/input/competitions'))\nprint(os.listdir('/kaggle/input/competitions/image-matching-challenge-2025'))\ndataset_path = \"/kaggle/input/competitions/image-matching-challenge-2025\"\nsample_submission = os.path.join(dataset_path, 'sample_submission.csv')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:44.785777Z","iopub.execute_input":"2026-06-28T15:00:44.786953Z","iopub.status.idle":"2026-06-28T15:00:44.796070Z","shell.execute_reply.started":"2026-06-28T15:00:44.786918Z","shell.execute_reply":"2026-06-28T15:00:44.795190Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, sys, glob, shutil, torch\n\n# find the dataset that holds lightglue\nhits = glob.glob(\"/kaggle/input/**/lightglue/__init__.py\", recursive=True)\nassert hits, \"Δεν βρέθηκε το lightglue package — έλεγξε το Add Input.\"\npkg_parent = os.path.dirname(os.path.dirname(hits[0]))   # -> .../LightGlue-main\nsys.path.insert(0, pkg_parent)\nprint(\"repo:\", pkg_parent)\n\n# copy the pretrained weights\nLG_WEIGHTS = {\n    \"superpoint_v1.pth\", \"aliked-n16.pth\",\n    \"superpoint_lightglue_v0-1_arxiv.pth\",\n    \"aliked_lightglue_v0-1_arxiv.pth\",\n    \"sift_lightglue_v0-1_arxiv.pth\",\n}\ndst = os.path.join(torch.hub.get_dir(), \"checkpoints\")\nos.makedirs(dst, exist_ok=True)\nfor f in glob.glob(\"/kaggle/input/**/*.pth\", recursive=True):\n    if os.path.basename(f) in LG_WEIGHTS:\n        tgt = os.path.join(dst, os.path.basename(f))\n        if not os.path.exists(tgt):\n            shutil.copy(f, tgt)\nprint(\"weights:\", sorted(os.listdir(dst)))\n\n# check if everything works\nfrom lightglue import LightGlue, SuperPoint, ALIKED, SIFT\nSuperPoint(max_num_keypoints=128).eval()\nALIKED(max_num_keypoints=128).eval()\nLightGlue(features=\"superpoint\").eval()\nLightGlue(features=\"aliked\").eval()        \nprint(\"LightGlue (superpoint + aliked) offline: OK\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:44.798152Z","iopub.execute_input":"2026-06-28T15:00:44.798554Z","iopub.status.idle":"2026-06-28T15:00:46.148448Z","shell.execute_reply.started":"2026-06-28T15:00:44.798527Z","shell.execute_reply":"2026-06-28T15:00:46.147580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"RUNNING = False","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:46.149480Z","iopub.execute_input":"2026-06-28T15:00:46.149777Z","iopub.status.idle":"2026-06-28T15:00:46.153642Z","shell.execute_reply.started":"2026-06-28T15:00:46.149748Z","shell.execute_reply":"2026-06-28T15:00:46.152799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install --no-index /kaggle/input/notebooks/stpeteishii/pycolmap-4-0-3-cp312/dist/pycolmap-4.0.3-cp312-cp312-manylinux_2_28_x86_64.whl --no-deps","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:46.154687Z","iopub.execute_input":"2026-06-28T15:00:46.155626Z","iopub.status.idle":"2026-06-28T15:00:47.943302Z","shell.execute_reply.started":"2026-06-28T15:00:46.155595Z","shell.execute_reply":"2026-06-28T15:00:47.942354Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pycolmap","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:47.946380Z","iopub.execute_input":"2026-06-28T15:00:47.946770Z","iopub.status.idle":"2026-06-28T15:00:47.951427Z","shell.execute_reply.started":"2026-06-28T15:00:47.946734Z","shell.execute_reply":"2026-06-28T15:00:47.950664Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"GPU = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:47.952528Z","iopub.execute_input":"2026-06-28T15:00:47.952779Z","iopub.status.idle":"2026-06-28T15:00:47.971119Z","shell.execute_reply.started":"2026-06-28T15:00:47.952756Z","shell.execute_reply":"2026-06-28T15:00:47.970275Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_path = os.path.join(dataset_path, 'train')\ntest_path = os.path.join(dataset_path, 'test')\n\ntrain_thresholds = pd.read_csv(os.path.join(dataset_path, 'train_thresholds.csv'))\ntrain_labels = pd.read_csv(os.path.join(dataset_path, 'train_labels.csv'))\n\ndisplay(train_thresholds.head())\ndisplay(train_labels.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:47.972242Z","iopub.execute_input":"2026-06-28T15:00:47.972607Z","iopub.status.idle":"2026-06-28T15:00:48.024443Z","shell.execute_reply.started":"2026-06-28T15:00:47.972566Z","shell.execute_reply":"2026-06-28T15:00:48.023600Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_scenes = train_labels[\"scene\"].nunique()\nprint(f'Scenes in train set: {num_scenes}')\n\nscene_counts = train_labels[\"scene\"].value_counts()\nscene_counts","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:48.025540Z","iopub.execute_input":"2026-06-28T15:00:48.026043Z","iopub.status.idle":"2026-06-28T15:00:48.038328Z","shell.execute_reply.started":"2026-06-28T15:00:48.025983Z","shell.execute_reply":"2026-06-28T15:00:48.037256Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import seaborn as sns\nfrom matplotlib import pyplot as plt\n\n# Counting images per dataset\ndataset_counts = train_labels[\"dataset\"].value_counts()\n\n# Plotting the bar chart\nplt.figure(figsize=(12, 6))  # Set figure size\nsns.barplot(x=dataset_counts.index, y=dataset_counts.values, palette=\"viridis\")  # Use seaborn for better styling\n\n# Adding labels and title\nplt.xlabel(\"Dataset\", fontsize=14)\nplt.ylabel(\"Number of Images\", fontsize=14)\nplt.title(\"Number of Images per Dataset\", fontsize=16)\nplt.xticks(rotation=45, ha=\"right\")  # Rotate x-axis labels for better readability\nplt.grid(axis=\"y\", linestyle=\"--\", alpha=0.7)  # Add a light grid for clarity\n\n# Show the plot\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:48.039584Z","iopub.execute_input":"2026-06-28T15:00:48.040094Z","iopub.status.idle":"2026-06-28T15:00:48.388797Z","shell.execute_reply.started":"2026-06-28T15:00:48.040066Z","shell.execute_reply":"2026-06-28T15:00:48.387478Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2  # OpenCV library for image processing\n\n# Pick a scene from the dataset\nscene_name = \"ET\"  \nscene_images = train_labels[train_labels[\"scene\"] == scene_name][\"image\"].values[:2]  # Picked the first two images of scene ET\n\n# Load and display images\nfig, axes = plt.subplots(1, 2, figsize=(10, 5))\n\nfor i, img_name in enumerate(scene_images):\n    img_path = os.path.join(train_path, train_labels[train_labels[\"scene\"] == scene_name][\"dataset\"].values[0], img_name)\n    img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)  # Loaded image in grayscale (easier for feature matching)\n    \n    axes[i].imshow(img, cmap=\"gray\")\n    axes[i].set_title(f\"Image: {img_name}\")\n    axes[i].axis(\"off\")\n\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:48.390128Z","iopub.execute_input":"2026-06-28T15:00:48.390578Z","iopub.status.idle":"2026-06-28T15:00:48.736551Z","shell.execute_reply.started":"2026-06-28T15:00:48.390549Z","shell.execute_reply":"2026-06-28T15:00:48.735501Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load the two images for matching\nimg1 = cv2.imread(os.path.join(train_path, train_labels[train_labels[\"scene\"] == scene_name][\"dataset\"].values[0], scene_images[0]), cv2.IMREAD_GRAYSCALE)\nimg2 = cv2.imread(os.path.join(train_path, train_labels[train_labels[\"scene\"] == scene_name][\"dataset\"].values[0], scene_images[1]), cv2.IMREAD_GRAYSCALE)\n\n# Initialize ORB detector\norb = cv2.ORB_create()\n\n# Detect keypoints and descriptors\nkp1, des1 = orb.detectAndCompute(img1, None)\nkp2, des2 = orb.detectAndCompute(img2, None)\n\n# Initialize Brute-Force Matcher and match descriptors\nbf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)\nmatches = bf.match(des1, des2)\n\n# Sort matches by distance (lower distance = better match)\nmatches = sorted(matches, key=lambda x: x.distance)\n\n# Draw matches\nmatch_img = cv2.drawMatches(img1, kp1, img2, kp2, [x for x in matches if x.distance < 50.0], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)\n\n# Display the matching result\nplt.figure(figsize=(12, 6))\nplt.imshow(match_img)\nplt.title(\"Feature Matching using ORB\")\nplt.axis(\"off\")\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:48.738006Z","iopub.execute_input":"2026-06-28T15:00:48.738346Z","iopub.status.idle":"2026-06-28T15:00:49.035662Z","shell.execute_reply.started":"2026-06-28T15:00:48.738311Z","shell.execute_reply":"2026-06-28T15:00:49.034592Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"matches[-1].distance # print the maximum distance ( least matching )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.037346Z","iopub.execute_input":"2026-06-28T15:00:49.037891Z","iopub.status.idle":"2026-06-28T15:00:49.043824Z","shell.execute_reply.started":"2026-06-28T15:00:49.037859Z","shell.execute_reply":"2026-06-28T15:00:49.042763Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# *Retrieval*","metadata":{}},{"cell_type":"code","source":"import shutil\nimport subprocess\nimport torch\nfrom torchvision.models import efficientnet_b0, EfficientNet_B0_Weights\nimport torch.nn as nn\nimport timm\nfrom PIL import Image\nfrom timm.data import resolve_data_config\nfrom timm.data.transforms_factory import create_transform\nfrom abc import ABC, abstractmethod\n\n\"\"\"\nTo add a new image retrieval mechanism:\n\n1. Define a class ImageRetrieval<New>(ImageRetrieval), i.e. a class that inherits ImageRetrieval\n2. This class should have at least the methods defined in the parent class, i.e. the init the setup and the match\n   with the correct signature\n3. The class may add anything else it needs to operate (i.e. extra functions, variables, etc.)\n3. Add the class in ImageRetrievalStrategies in the next cell\n\"\"\"\n\nclass ImageRetrieval(ABC):\n    \"\"\"\n        ImageRetrieval should support two methods:\n            - setup(images, image_names): called during extract_features and sets up the image retrieval (i.e. extracting embeddings from images, clustering, etc)\n            - match(image1, image2): Takes 2 image names and returns whether they should be processed (i.e. belong in the same cluster or are similar)\n    \"\"\"\n    @abstractmethod\n    def __init__(self, args):\n        pass\n    \n    @abstractmethod\n    def setup(self, imgs, img_names) -> None:\n        pass\n        \n    @abstractmethod\n    def match(self, img1, img2) -> tuple[bool, float]:\n        pass\n\nclass ImageRetrievalAll(ImageRetrieval):\n    def __init__(self, args = None):\n        pass\n\n    def setup(self, imgs, img_names) -> None:\n        pass\n\n    def match(self, img1, img2) -> tuple[bool, float]:\n        return True, 1\n\nclass ImageRetrievalCNN(ImageRetrieval):\n    def __init__(self, args):\n        \"\"\"\n            Init takes the method for retrieval (all means no retrieval, CNN means the efficientnet+similarity)\n            and args which is a dictionary with any parameters we want to use.\n        \"\"\"\n        self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        self.args = args\n\n        self.effnet =  timm.create_model('tf_efficientnet_b0', checkpoint_path='/kaggle/input/models/timm/tf-efficientnet/pytorch/tf-efficientnet-b0/1/tf_efficientnet_b0_aa-827b6e33.pth').to(self.device).eval()\n        config = resolve_data_config({}, model=self.effnet)\n        self.cnn_preprocess = create_transform(**config) # preprocess ( crop etc) the images to be cut out for this neural network\n\n    \"\"\" Helper functions \"\"\"\n\n    def _preprocess_for_effnet(self, img):\n        img_pil = Image.fromarray(img)\n        img_tensor = self.cnn_preprocess(img_pil).to(self.device)\n        return img_tensor\n\n    \"\"\" Setup \"\"\"\n\n    def setup(self, imgs, img_names) -> None:\n        imgs_tensor = []\n        self.cnn_features = {}\n        for img in imgs:\n            img_tensor = self._preprocess_for_effnet(img)\n            imgs_tensor.append(img_tensor)\n\n        batch = torch.stack(imgs_tensor).to(self.device)\n        with torch.no_grad():\n            feat_map = self.effnet.forward_features(batch)\n\n        # pooled global descriptor of the input image\n        pooled = feat_map.mean(dim=(2, 3))  # [B, F]\n        pooled = pooled.cpu().numpy()\n\n        for img_name, feat in zip(img_names, pooled):\n            self.cnn_features[img_name] = feat # save the global descriptors in a dictionary\n        return\n\n    \"\"\" Match \"\"\"\n    \n    def match(self, img1, img2) -> tuple[bool, float]:\n        similarity = torch.cosine_similarity(\n            torch.from_numpy(self.cnn_features[img1]).flatten(), \n            torch.from_numpy(self.cnn_features[img2]).flatten(), \n            dim=0\n        ).item()\n        similarity_threshold = self.args['similarity_threshold']\n        return similarity >= similarity_threshold, similarity # if cosine similarity of the global descs is above thres -> return True\n        \n        \n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.045392Z","iopub.execute_input":"2026-06-28T15:00:49.045858Z","iopub.status.idle":"2026-06-28T15:00:49.064131Z","shell.execute_reply.started":"2026-06-28T15:00:49.045819Z","shell.execute_reply":"2026-06-28T15:00:49.063086Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\nRetrieval via SuperPoint + LightGlue  (pair gating by match count).\n  - resize every image so its longest side is `resize_long` (default 1280) px\n  - extract local features with SuperPoint\n  - match each pair with LightGlue, KEEP it only if n_matches >= `min_matches` (30)\nNOTE: used purely as a PAIR FILTER for now\n\"\"\"\nimport cv2\nimport numpy as np\nimport torch\nfrom lightglue import LightGlue, SuperPoint\nfrom lightglue.utils import numpy_image_to_torch, rbd\n\n\nclass ImageRetrievalSuperPoint(ImageRetrieval):\n    def __init__(self, args=None):\n        args = args or {}\n        self.args = args\n        self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        self.min_matches       = args.get(\"min_matches\", 30)\n        self.resize_long       = args.get(\"resize_long\", 1280)\n        self.max_num_keypoints = args.get(\"max_num_keypoints\", 2048)\n        self.downscale_only    = args.get(\"downscale_only\", True)\n        self.extractor = SuperPoint(max_num_keypoints=self.max_num_keypoints).eval().to(self.device)\n        self.matcher = LightGlue(features=\"superpoint\").eval().to(self.device)\n        self.feats = {}\n        self.keypoints_xy = {}   # name -> np.float32 [N,2] στο resized frame\n        self.image_hw     = {}   # name -> (h, w) του resized frame\n        self.pair_matches = {}   # (a,b) -> np.uint32 [K,2] index pairs (LightGlue)\n\n    def _to_tensor(self, img_bgr):\n        img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)\n        h, w = img.shape[:2]\n        long_side = max(h, w)\n        need = (long_side > self.resize_long) or (not self.downscale_only and long_side != self.resize_long)\n        if need:\n            scale = self.resize_long / long_side\n            nw, nh = max(1, int(round(w * scale))), max(1, int(round(h * scale)))\n            interp = cv2.INTER_AREA if scale < 1 else cv2.INTER_CUBIC\n            img = cv2.resize(img, (nw, nh), interpolation=interp)\n        return numpy_image_to_torch(img).to(self.device)\n\n    def setup(self, imgs, img_names) -> None:\n        self.feats, self.keypoints_xy, self.image_hw, self.pair_matches = {}, {}, {}, {}\n        with torch.inference_mode():\n            for img, name in zip(imgs, img_names):\n                if img is None:\n                    continue\n                t = self._to_tensor(img)                      # [3, H, W] resized\n                feats = self.extractor.extract(t)\n                self.feats[name] = feats\n                kp = feats[\"keypoints\"][0].detach().cpu().numpy().astype(np.float32)  # [N,2] resized coords\n                self.keypoints_xy[name] = kp\n                self.image_hw[name] = (int(t.shape[-2]), int(t.shape[-1]))            # (h, w)\n\n    def match(self, img1, img2):\n        f1, f2 = self.feats.get(img1), self.feats.get(img2)\n        if f1 is None or f2 is None:\n            return False, 0.0\n        with torch.inference_mode():\n            out = self.matcher({\"image0\": f1, \"image1\": f2})\n        m = rbd(out)[\"matches\"]                                # [K,2] indices\n        n = int(m.shape[0])\n        if n >= self.min_matches:\n            self.pair_matches[(img1, img2)] = m.detach().cpu().numpy().astype(np.uint32)\n        return n >= self.min_matches, float(n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.068006Z","iopub.execute_input":"2026-06-28T15:00:49.068413Z","iopub.status.idle":"2026-06-28T15:00:49.091296Z","shell.execute_reply.started":"2026-06-28T15:00:49.068380Z","shell.execute_reply":"2026-06-28T15:00:49.090191Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# *Matching & Graph Construction*","metadata":{}},{"cell_type":"code","source":"import shutil\nimport subprocess\nimport torch\nfrom torchvision.models import efficientnet_b0, EfficientNet_B0_Weights\nimport torch.nn as nn\nimport timm\nfrom PIL import Image\nfrom timm.data import resolve_data_config\nfrom timm.data.transforms_factory import create_transform\n\n\"\"\"Supported ImageRetrievals\"\"\"\nImageRetrievalStrategies = {\n    \"All\": ImageRetrievalAll,\n    \"CNN\": ImageRetrievalCNN,\n    \"SuperPoint\": ImageRetrievalSuperPoint\n}\n\n\n\nclass ImageMatchingDataset:\n    image_names: list[str]          # List of paths to all images in dataset (without the dataset itself)\n    detections: dict[str, list]     # image_path -> list of features\n    def __init__(self, dataset, nfeatures = 5000, feature = 'orb', min_matches=15, matcher = 'bf', use_knn=False, train=False, use_custom_features = False, workspace = 'colmap_pipeline_workspace_2', use_cnn_filtering = True, similarity_threshold = 0.5, retrieval = None, retrieval_args = None):\n        self.min_matches = min_matches\n        self.matcher = matcher\n        self.feature_extractor = feature\n        self.use_knn = use_knn\n        self.train = train\n        self.use_cnn_filtering = use_cnn_filtering\n        self.nfeatures = nfeatures\n        self.colmap_exe = r\"C:\\Program Files\\COLMAP\\COLMAP.bat\"\n        self.workspace_root = workspace\n        self.use_custom_features = use_custom_features\n        self.similarity_threshold = similarity_threshold\n\n        \n\n        # --- Retrieval component selection (config-driven via the registry) ---\n        # Backward compatible: if `retrieval` is not given we fall back to the old\n        # `use_cnn_filtering` toggle, so every existing call behaves exactly as before.\n        if retrieval is None:\n            retrieval = \"CNN\" if use_cnn_filtering else \"All\"\n        if retrieval not in ImageRetrievalStrategies:\n            raise ValueError(f\"Unknown retrieval '{retrieval}'. Available: {list(ImageRetrievalStrategies.keys())}\")\n        if retrieval_args is None:\n            retrieval_args = {\"similarity_threshold\": similarity_threshold} if retrieval == \"CNN\" else {}\n        self.retrieval_name = retrieval\n        self.imageretrieval: ImageRetrieval = ImageRetrievalStrategies[retrieval](retrieval_args)\n        \n        # Setup EfficientNetB0 for similarity-based matching\n        # if use_cnn_filtering:\n        #     self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        #     self.effnet =  timm.create_model('tf_efficientnet_b0', checkpoint_path='/kaggle/input/models/timm/tf-efficientnet/pytorch/tf-efficientnet-b0/1/tf_efficientnet_b0_aa-827b6e33.pth').to(self.device).eval()\n        #     # self.cnn_feature_extractor = nn.Sequential(\n        #     #     self.effnet.features,\n        #     #     self.effnet.avgpool\n        #     # )\n        #     # with torch.no_grad():\n        #     #     feat_map = self.effnet.forward_features(images)\n            \n        #     # pooled = feat_map.mean(dim=(2, 3))\n        #     # self.cnn_feature_extractor = pooled\n        #     # self.cnn_preprocess = EfficientNet_B0_Weights.IMAGENET1K_V1.transforms()\n        #     config = resolve_data_config({}, model=self.effnet)\n        #     self.cnn_preprocess = create_transform(**config)\n                    \n        try:\n            files = os.listdir(dataset)\n        except Exception as e:\n            print(f'Error listing dataset files: {repr(e)}')\n        \n        self.dataset_path = dataset\n        self.image_names = [x for x in sorted(files) if x != 'LICENSE.txt']\n        \n        if self.train:\n            self.group_images_by_train_labels()\n          \n    @staticmethod  \n    def preprocess_image(img_path, target_long_side=1600):\n        # 1. Load image in grayscale (SIFT only needs intensity data)\n        img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)\n        img_color = cv2.imread(img_path, cv2.IMREAD_COLOR) # keep the color to be used in colmap\n        if img is None:\n            return None\n            \n        # 2. Resize while maintaining aspect ratio\n        h, w = img.shape[:2]\n        long_side = max(h, w)\n        if long_side > target_long_side: ##################\n            scale = target_long_side / long_side\n            new_w, new_h = int(w * scale), int(h * scale)\n            img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_CUBIC)\n        \n        # 3. Apply CLAHE to boost local distinctiveness\n        # clipLimit=2.0 and tileGridSize=(8,8) are solid general defaults\n        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))\n        img_enhanced = clahe.apply(img)\n        \n        return img_enhanced, img_color\n    \n    \n    def extract_features(self):\n        if self.feature_extractor == 'orb':\n            feature_extractor = cv2.ORB_create(nfeatures=self.nfeatures, fastThreshold=5)\n            extraction_function = feature_extractor.detectAndCompute\n        elif self.feature_extractor == 'sift':\n            feature_extractor = cv2.SIFT_create(nfeatures=self.nfeatures, edgeThreshold=10, contrastThreshold=0.04)\n            extraction_function = feature_extractor.detectAndCompute\n        else:\n            raise ValueError(\"Unsupported feature extractor\")\n        self.detections = {}\n        self.cnn_features = {}\n\n        imgs = []\n        img_names = []\n\n        for img_name in self.image_names:\n            img_path = os.path.join(self.dataset_path, img_name)\n            img, img_color = self.preprocess_image(img_path)\n\n            imgs.append(img_color)\n            img_names.append(img_name)\n            \n            if img is None:\n                continue\n\n            kp, des = extraction_function(img, None)\n            \n            # RootSIFT normalization for SIFT descriptors\n            if self.feature_extractor == 'sift' and des is not None:\n                des /= (np.linalg.norm(des, ord=1, axis=1, keepdims=True) + 1e-10)\n                des = np.sqrt(des)\n            self.detections[img_name] = {\"keypoints\": kp, \"descriptors\": des}\n            if len(kp)<self.min_matches:\n                print(f\"Warning: Only {len(kp)} keypoints detected in {img_name}, which is below the minimum threshold of {self.min_matches}. This image may be excluded from matching.\")\n                # Plot the image with keypoints\n                img_kp = cv2.drawKeypoints(img, kp, None, color=(0,255,0), flags=cv2.DrawMatchesFlags_DEFAULT)\n                plt.figure(figsize=(8, 6))\n                plt.imshow(img_kp, cmap='gray')\n                plt.title(f\"{img_name} - {len(kp)} keypoints\")\n                plt.axis('off')\n                plt.show()\n\n        # if self.use_cnn_filtering:\n        #     batch = torch.stack(imgs).to(self.device)\n        #     with torch.no_grad():\n        #         feat_map = self.effnet.forward_features(batch)\n                \n        #     pooled = feat_map.mean(dim=(2, 3))  # [B, F]\n        #     pooled = pooled.cpu().numpy()\n    \n        #     for img_name, feat in zip(img_names, pooled):\n        #         self.cnn_features[img_name] = feat\n\n        self.imageretrieval.setup(imgs, img_names)\n        \n        self.image_names = [n for n in self.image_names if n in self.detections]\n        return self.detections\n    \n    def filter_matches(self, matches, ratio=0.75, dist_threshold=30):\n        \"\"\" Filters matches using Lowe's ratio test. \"\"\"\n        good_matches = []\n        if self.use_knn:\n            for pair in matches:\n                if len(pair) < 2:\n                    continue\n                m, n = pair\n                if m.distance < ratio * n.distance:\n                    good_matches.append(m)\n        else:\n            for m in sorted(matches, key=lambda x: x.distance):\n                if m.distance < dist_threshold:  \n                    good_matches.append(m)\n        return good_matches\n    \n    def ransac_filter(self):\n        self.inlier_info = []\n\n        for match in self.matches_info:\n            try:\n                img_name1, img_name2 = match[\"image_pair\"]\n                kp1 = self.detections[img_name1][\"keypoints\"]\n                kp2 = self.detections[img_name2][\"keypoints\"]\n                matches = match[\"matches\"]\n    \n                pts1 = np.float32([kp1[m.queryIdx].pt for m in matches])\n                pts2 = np.float32([kp2[m.trainIdx].pt for m in matches])\n    \n                if len(pts1) < 8:\n                    continue\n    \n                F, mask = cv2.findFundamentalMat(\n                    pts1, \n                    pts2, \n                    method=cv2.USAC_MAGSAC, \n                    ransacReprojThreshold=1.5, # Strict spatial consensus\n                    confidence=0.999,          # High confidence to reject random alignments\n                    maxIters=5000\n                )\n                if mask is None:\n                    continue\n    \n                inliers = int(mask.ravel().sum())\n                if inliers < self.min_matches:\n                    continue\n    \n                self.inlier_info.append({\n                    \"image_pair\": (img_name1, img_name2),\n                    \"num_inliers\": inliers,\n                    \"num_matches\": len(matches),\n                })\n            except:\n                continue\n\n        return self.inlier_info\n    \n    def pairwise_match(self):\n        self.matches_info = []\n        valid_names = self.image_names\n        \n        self.similarities = np.zeros((len(valid_names), len(valid_names)))\n\n        for i in range(len(valid_names)):\n            for j in range(i + 1, len(valid_names)):\n                img_name1, img_name2 = valid_names[i], valid_names[j]\n                # Compute similarity and skip if low\n                # if self.use_cnn_filtering:\n                #     similarity = torch.cosine_similarity(\n                #         torch.from_numpy(self.cnn_features[img_name1]).flatten(), \n                #         torch.from_numpy(self.cnn_features[img_name2]).flatten(), \n                #         dim=0\n                #     ).item()\n                # else:\n                #     similarity = 1.0  # or some default value\n                # self.similarities[i, j] = similarity\n                # self.similarities[j, i] = similarity\n                # if similarity < self.similarity_threshold:  # Threshold can be tuned based on dataset characteristics\n                #     # print(f\"Skipping pair ({img_name1}, {img_name2}) due to low similarity: {similarity:.4f}\")\n                #     continue\n\n                match, similarity = self.imageretrieval.match(img_name1, img_name2) # if pair doesnt match we will skip it\n                self.similarities[i, j] = similarity\n                self.similarities[j, i] = similarity\n                if not match:\n                    continue\n        \n                \n                des1 = self.detections[img_name1][\"descriptors\"]\n                des2 = self.detections[img_name2][\"descriptors\"]\n                if des1 is None or des2 is None:\n                    continue\n\n                if self.matcher == 'bf':\n                    matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=not self.use_knn)\n                elif self.matcher == 'flann':\n                    if self.feature_extractor == 'orb':\n                        algorithm = 6  # FLANN_INDEX_LSH\n                    elif self.feature_extractor == 'sift':\n                        algorithm = 1  # FLANN_INDEX_KDTREE\n                    else:\n                        raise ValueError(\"Unsupported feature type for FLANN matcher\")\n                    index_params = dict(algorithm=algorithm,  # FLANN_INDEX_LSH\n                                        table_number=6,\n                                        key_size=12,\n                                        multi_probe_level=1)\n                    search_params = dict(checks=50)\n                    matcher = cv2.FlannBasedMatcher(index_params, search_params)\n                else:\n                    raise ValueError(f\"Unsupported matcher type: {self.matcher}\")\n\n                try:\n                    if self.use_knn:\n                        matches = matcher.knnMatch(des1, des2, k=2)\n                    else:\n                        matches = matcher.match(des1, des2)\n                    matches = self.filter_matches(matches)\n                except:\n                    continue\n\n                self.matches_info.append({\n                    \"image_pair\": (img_name1, img_name2),\n                    \"num_matches\": len(matches),\n                    \"matches\": matches,\n                })\n\n        return self.matches_info\n    \n    def prune_inliers(self):\n        new_inlier_info = []\n        for info in self.inlier_info:\n            if info[\"num_inliers\"] >= self.min_matches:\n                new_inlier_info.append(info)\n        self.inlier_info = new_inlier_info\n        return self.inlier_info\n    \n    def visualize_matches(self):\n        # Visualize number of matches for each image pair in a heatmap grid where x is images and y is images, and the value is the number of matches        \n        grid_values = np.zeros((len(self.image_names), len(self.image_names)), dtype=int)\n        for info in self.inlier_info:\n            img_name1, img_name2 = info[\"image_pair\"]\n            idx1 = self.image_names.index(img_name1)\n            idx2 = self.image_names.index(img_name2)\n            grid_values[idx1, idx2] = info[\"num_inliers\"]\n            grid_values[idx2, idx1] = info[\"num_inliers\"]\n            \n        plt.figure(figsize=(10, 8))\n        sns.heatmap(grid_values, xticklabels=self.image_names, yticklabels=self.image_names, annot=True, fmt=\"d\", cmap=\"YlGnBu\")\n        plt.title(\"Number of Inliers Between Image Pairs\")\n        plt.xlabel(\"Image\")\n        plt.ylabel(\"Image\")\n        plt.xticks([])\n        # plt.yticks(rotation=0)\n        # Color yticks based on assigned clusters if available\n        if hasattr(self, 'clusters'):\n            cluster_colors = sns.color_palette(\"hsv\", len(self.clusters))\n            img_to_cluster = {}\n            for cluster_idx, cluster in enumerate(self.clusters):\n                for img_name in cluster:\n                    img_to_cluster[img_name] = cluster_idx\n            for ytick in plt.gca().get_yticklabels():\n                img_name = ytick.get_text()\n                if img_name in img_to_cluster:\n                    cluster_idx = img_to_cluster[img_name]\n                    ytick.set_color(cluster_colors[cluster_idx])\n        plt.tight_layout()\n        plt.show()\n        # Plot similarity heatmap without annotations\n        plt.figure(figsize=(10, 8))\n        sns.heatmap(self.similarities, xticklabels=self.image_names, yticklabels=self.image_names, cmap=\"YlOrRd\")\n        plt.title(\"Cosine Similarity Between Image Pairs\")\n        plt.xlabel(\"Image\")\n        plt.ylabel(\"Image\")\n        plt.tight_layout()\n        plt.show()\n\n    def group_images_by_train_labels(self, labels_df=train_labels, include_unlabeled=False):\n        \"\"\"\n        Group images in this dataset according to train_labels (columns: dataset, scene, image, rotation_matrix, ...)\n        Returns dict: scene -> list of dicts { \"image\": <filename>, \"rotation_matrix\": <str>, \"translation_vector\": <str> }\n        \"\"\"\n        ds_name = os.path.basename(self.dataset_path)\n        rows = labels_df[labels_df['dataset'] == ds_name]\n        groups = {}\n        # map image -> metadata from labels\n        for _, row in rows.iterrows():\n            img_name = row['image']\n            if img_name not in self.image_names:\n                continue\n            scene = row['scene']\n            meta = {\n                \"image\": img_name,\n                \"rotation_matrix\": row.get('rotation_matrix') if 'rotation_matrix' in row.index else None,\n                \"translation_vector\": row.get('translation_vector') if 'translation_vector' in row.index else None\n            }\n            groups.setdefault(scene, []).append(meta)\n\n        if include_unlabeled:\n            unlabeled = [n for n in self.image_names if n not in rows['image'].values]\n            if unlabeled:\n                groups.setdefault('__unlabeled__', []).extend([{\"image\": n, \"rotation_matrix\": None, \"translation_vector\": None} for n in unlabeled])\n\n        self.grouped_by_scene = groups\n        return groups\n    \n    def cluster_images(self):\n        self.G = nx.Graph()\n        self.G.add_nodes_from(self.image_names)\n        \n        # # Keep only top 3 edge of each node-image. Calculate threshold for each image:\n        # for img_name in self.image_names:\n        #     edges = []\n        #     for info in self.inlier_info:\n        #         img_name1, img_name2 = info[\"image_pair\"]\n        #         if img_name1 == img_name:\n        #             edges.append((img_name2, info[\"num_inliers\"]))\n        #         elif img_name2 == img_name:\n        #             edges.append((img_name1, info[\"num_inliers\"]))\n        #     edges = sorted(edges, key=lambda x: x[1], reverse=True)[:3]\n        #     for neighbor, weight in edges:\n        #         self.G.add_edge(img_name, neighbor, weight=weight)\n        \n        scores = [info[\"num_inliers\"] for info in self.inlier_info]\n        noise_level = np.median(scores) if scores else 0\n        print(f\"Estimated noise level (median inliers): {noise_level}\")\n        # threshold = noise_level * 1.0\n        threshold = self.min_matches\n\n        for info in self.inlier_info:\n            img_name1, img_name2 = info[\"image_pair\"]\n            if info[\"num_inliers\"] >= threshold:\n                self.G.add_edge(img_name1, img_name2, weight=info[\"num_inliers\"])\n\n        self.clusters = [sorted(list(c)) for c in nx.connected_components(self.G) if len(c) > 0]\n        # communities = nx.algorithms.community.louvain_communities(self.G, weight='weight', resolution = 0.3)\n        # self.clusters = [sorted(list(c)) for c in communities if len(c) > 0]\n        return self.clusters\n    \n    def evaluate_clusters(self):\n        \"\"\"If train, evaluate clusters against train_labels grouping by scene. Returns dict: cluster -> most common scene in that cluster. and prints metrics like purity, NMI, ARI.\"\"\"\n        if not self.train:\n            print(\"Evaluation only available for training datasets with labels.\")\n            return None\n        \n        # Match clusters to scenes\n        cluster_to_scene = {}\n        for cluster in self.clusters:\n            scene_counts = defaultdict(int)\n            for img_name in cluster:\n                for scene, images in self.grouped_by_scene.items():\n                    if any(img_info[\"image\"] == img_name for img_info in images):\n                        scene_counts[scene] += 1\n            if scene_counts:\n                best_scene = max(scene_counts, key=scene_counts.get)\n                cluster_to_scene[tuple(cluster)] = best_scene\n                \n        # Compute metrics\n        from sklearn.metrics import normalized_mutual_info_score, adjusted_rand_score\n        true_labels = []\n        pred_labels = []\n        for cluster, scene in cluster_to_scene.items():\n            for img_name in cluster:\n                true_scene = None\n                for s, images in self.grouped_by_scene.items():\n                    if any(img_info[\"image\"] == img_name for img_info in images):\n                        true_scene = s\n                        break\n                if true_scene is not None:\n                    true_labels.append(true_scene)\n                    pred_labels.append(scene)\n        nmi = normalized_mutual_info_score(true_labels, pred_labels)\n        ari = adjusted_rand_score(true_labels, pred_labels)\n        print(f\"Cluster Evaluation Metrics: NMI={nmi:.4f}, ARI={ari:.4f}\")\n        return cluster_to_scene\n    \n    def _prepare_env(self):\n        env = os.environ.copy()\n        env[\"CUDA_VISIBLE_DEVICES\"] = \"-1\"\n        env[\"COLMAP_DISABLE_CUDA\"] = \"1\"\n        return env\n\n    def _is_image_file(self, name):\n        return name.lower().endswith((\".jpg\", \".jpeg\", \".png\", \".bmp\", \".tif\", \".tiff\", \".webp\"))\n\n    def _cluster_workspace(self, cluster_idx):\n        cluster_ws = os.path.join(self.workspace_root, f\"cluster_{cluster_idx}\")\n        db_path = os.path.join(cluster_ws, \"database.db\")\n        sparse_path = os.path.join(cluster_ws, \"sparse\")\n        img_list_path = os.path.join(cluster_ws, \"images.txt\")\n\n        os.makedirs(cluster_ws, exist_ok=True)\n        if os.path.exists(db_path):\n            os.remove(db_path)\n        if os.path.exists(sparse_path):\n            shutil.rmtree(sparse_path)\n        os.makedirs(sparse_path, exist_ok=True)\n\n        return cluster_ws, db_path, sparse_path, img_list_path\n\n    def _write_image_list(self, cluster, img_list_path):\n        with open(img_list_path, \"w\", encoding=\"utf-8\") as f:\n            for img_name in cluster:\n                f.write(img_name + \"\\n\")\n\n    def _run_colmap_cmd(self, args):\n        return subprocess.run(\n            args,\n            env=self.env,\n            capture_output=True,\n            text=True,\n        )\n\n    def _extract_pose_from_image(self, im):\n        cfw = getattr(im, \"cam_from_world\", None)\n        if cfw is not None:\n            T = cfw() if callable(cfw) else cfw\n\n            matrix_attr = getattr(T, \"matrix\", None)\n            if matrix_attr is not None:\n                M = np.asarray(matrix_attr() if callable(matrix_attr) else matrix_attr, dtype=float)\n                if M.shape == (3, 4):\n                    return M[:, :3], M[:, 3]\n                if M.shape == (4, 4):\n                    return M[:3, :3], M[:3, 3]\n\n            rot_attr = getattr(T, \"rotation\", None)\n            trans_attr = getattr(T, \"translation\", None)\n            if rot_attr is not None and trans_attr is not None:\n                rot_obj = rot_attr() if callable(rot_attr) else rot_attr\n                trans = np.asarray(trans_attr() if callable(trans_attr) else trans_attr, dtype=float).reshape(3)\n\n                rot_matrix_attr = getattr(rot_obj, \"matrix\", None)\n                if rot_matrix_attr is not None:\n                    R = np.asarray(rot_matrix_attr() if callable(rot_matrix_attr) else rot_matrix_attr, dtype=float)\n                    return R, trans\n\n                quat_attr = getattr(rot_obj, \"quat\", None)\n                if quat_attr is not None:\n                    q = np.asarray(quat_attr() if callable(quat_attr) else quat_attr, dtype=float).reshape(4)\n                    qw, qx, qy, qz = q\n                    R = np.array([\n                        [1 - 2 * (qy * qy + qz * qz), 2 * (qx * qy - qz * qw),     2 * (qx * qz + qy * qw)],\n                        [2 * (qx * qy + qz * qw),     1 - 2 * (qx * qx + qz * qz), 2 * (qy * qz - qx * qw)],\n                        [2 * (qx * qz - qy * qw),     2 * (qy * qz + qx * qw),     1 - 2 * (qx * qx + qy * qy)],\n                    ], dtype=float)\n                    return R, trans\n\n        q_attr = getattr(im, \"qvec\", None)\n        t_attr = getattr(im, \"tvec\", None)\n        if q_attr is not None and t_attr is not None:\n            q = np.asarray(q_attr() if callable(q_attr) else q_attr, dtype=float).reshape(4)\n            t = np.asarray(t_attr() if callable(t_attr) else t_attr, dtype=float).reshape(3)\n            qw, qx, qy, qz = q\n            R = np.array([\n                [1 - 2 * (qy * qy + qz * qz), 2 * (qx * qy - qz * qw),     2 * (qx * qz + qy * qw)],\n                [2 * (qx * qy + qz * qw),     1 - 2 * (qx * qx + qz * qz), 2 * (qy * qz - qx * qw)],\n                [2 * (qx * qz - qy * qw),     2 * (qy * qz + qx * qw),     1 - 2 * (qx * qx + qy * qy)],\n            ], dtype=float)\n            return R, t\n\n        raise RuntimeError(\"Could not extract pose from pycolmap image object.\")\n    \n    def _run_colmap_with_own_features(self, cluster_idx, cluster):\n        cluster_ws, db_path, sparse_path, img_list_path = self._cluster_workspace(cluster_idx)\n        self._write_image_list(cluster, img_list_path)\n        # db_path = 'colmap_custom_1/database.db'\n        db = pycolmap.Database.open(db_path)\n        self.db = db\n        \n        print(f\"Created database for cluster {cluster_idx} at {db_path}.\")\n        \n        height = self.detections[cluster[0]][\"keypoints\"][0].size if self.detections[cluster[0]][\"keypoints\"] else 480\n        width = self.detections[cluster[0]][\"keypoints\"][0].size if self.detections[cluster[0]][\"keypoints\"] else 640\n        cx, cy = width / 2, height / 2\n        f = 1.2 * max(width, height)\n        k = 0.0\n        \n        def get_camera(camera_id):\n            # camera_id = 0  # or any integer (not used until write)\n\n            camera = pycolmap.Camera.create_from_model_id(\n                camera_id,\n                pycolmap.CameraModelId.SIMPLE_RADIAL,\n                float(f),\n                int(width),\n                int(height),\n            )\n            # camera.params = [f, width / 2, height / 2, 0.0]\n            camera_id = db.write_camera(camera)\n            return camera_id\n        # print(f\"Added camera to database for cluster {cluster_idx} with ID {camera_id}\")\n        \n        def cv2_keypoints_to_colmap(kps):\n            # print(kps, kps.pt)\n            return np.array(\n                [(kp.pt[0], kp.pt[1]) for kp in kps],\n                dtype=np.float32\n            )\n            \n        def prepare_descriptors(desc):\n            return pycolmap.FeatureDescriptors(\n                {\n                    \"data\": np.ascontiguousarray(desc, dtype=np.uint8)\n                }\n            )\n            \n        image_name_to_id = {}\n        \n        # Add images to database\n        for i, img_name in enumerate(cluster):\n            camera_id = get_camera(i)\n            image = pycolmap.Image(\n                name=img_name,\n                camera_id=camera_id\n            )\n\n            image_id = db.write_image(image)\n            image_name_to_id[img_name] = image_id\n            des = self.detections[img_name][\"descriptors\"]\n            if des is not None:\n                db.write_keypoints(\n                    image_id = image_id,\n                    keypoints = cv2_keypoints_to_colmap(self.detections[img_name][\"keypoints\"])\n                )\n                db.write_descriptors(\n                    image_id = image_id,\n                    descriptors = prepare_descriptors(des)\n                )\n        print(f\"Added {len(cluster)} images and their features to database for cluster {cluster_idx}\")\n        \n        def cv_matches_to_colmap(dmatches):\n            matches = np.array(\n                [(m.queryIdx, m.trainIdx) for m in dmatches],\n                dtype=np.uint32\n            )\n            return matches\n                    \n        # Add matches to database\n        added = 0\n        for info in self.matches_info:\n            img_name1, img_name2 = info[\"image_pair\"]\n            if img_name1 in cluster and img_name2 in cluster:\n                try:\n                    img_id1 = image_name_to_id[img_name1]\n                    img_id2 = image_name_to_id[img_name2]\n                except KeyError:\n                    continue\n                matches = info[\"matches\"]\n                if len(matches) == 0:\n                    continue\n                \n                db.write_matches(\n                    image_id1 = img_id1,\n                    image_id2 = img_id2,\n                    matches = cv_matches_to_colmap(matches)\n                )\n                \n                added +=1\n            # db.commit()\n        print(f\"Added {added} matches to database for cluster {cluster_idx}\")\n        \n\n        print(\"num images:\", db.num_images())\n        print(\"num cameras:\", db.num_cameras())\n        print(\"num matches:\", db.num_matches())\n        print(\"num matched pairs:\", db.num_matched_image_pairs())\n        print(\"num keypoints:\", db.num_keypoints())\n        pairs = db.read_all_matches()\n        print(\"pairs:\", [len(x) for x in pairs])\n    \n        pycolmap.geometric_verification(db_path)\n        tvg = db.read_two_view_geometries()\n        print('two view geometries', [len(x) for x in tvg])\n        \n        opts = pycolmap.IncrementalPipelineOptions()\n\n        # opts.min_num_matches = 8\n        # opts.mapper.init_min_num_inliers = 15\n        # opts.mapper.init_max_error = 12\n        \n        class DebugCallback(pycolmap.IncrementalPipelineCallback):\n            def __init__(self):\n                super().__init__()\n\n            def __call__(self, status):\n                print(status)\n\n        maps = pycolmap.incremental_mapping(\n            database_path=db_path,\n            image_path=self.dataset_path,\n            output_path=sparse_path,\n            options=opts,\n        )\n        model_dirs = [\n            os.path.join(sparse_path, d)\n            for d in os.listdir(sparse_path)\n            if os.path.isdir(os.path.join(sparse_path, d))\n        ]\n        db.close()\n        return model_dirs[0] if model_dirs else None\n        \n    def _run_colmap_for_cluster(self, cluster_idx, cluster):\n        cluster_ws, db_path, sparse_path, img_list_path = self._cluster_workspace(cluster_idx)\n        self._write_image_list(cluster, img_list_path)\n        with open(img_list_path, \"r\") as f:\n            image_names = [line.strip() for line in f if line.strip()]\n\n        extraction_options = pycolmap.FeatureExtractionOptions()\n        # extraction_options.sift.max_num_features = 8\n        # extraction_options.sift.peak_threshold = 0.007\n\n        pycolmap.extract_features(\n            database_path=db_path,\n            image_path=self.dataset_path,\n            image_names=image_names,\n            camera_mode=pycolmap.CameraMode.AUTO,\n            extraction_options = extraction_options,\n            reader_options=pycolmap.ImageReaderOptions(\n                camera_model=\"SIMPLE_RADIAL\"\n            ),\n        )\n\n        matching_options = pycolmap.FeatureMatchingOptions(\n            guided_matching=True,\n        )\n            \n        pycolmap.match_exhaustive(\n            database_path=db_path,\n            matching_options=matching_options\n        )\n\n        opts = pycolmap.IncrementalPipelineOptions()\n\n        opts.min_num_matches = 8\n        opts.mapper.init_min_num_inliers = 15\n        opts.mapper.init_max_error = 12\n        \n        db = pycolmap.Database.open(db_path)\n        print(\"num images:\", db.num_images())\n        print(\"num cameras:\", db.num_cameras())\n        print(\"num matches:\", db.num_matches())\n        print(\"num matched pairs:\", db.num_matched_image_pairs())\n        print(\"num keypoints:\", db.num_keypoints())\n        pairs = db.read_all_matches()\n        print(\"pairs:\", [len(x) for x in pairs])\n        db.close()\n\n        maps = pycolmap.incremental_mapping(\n            database_path=db_path,\n            image_path=self.dataset_path,\n            output_path=sparse_path,\n            options=opts,\n        )\n\n        model_dirs = [\n            os.path.join(sparse_path, d)\n            for d in os.listdir(sparse_path)\n            if os.path.isdir(os.path.join(sparse_path, d))\n        ]\n        # Re-cluster based on which images are in which model directories, if len(model_dirs) > 1:\n        if len(model_dirs) > 1:\n            new_clusters = []\n            for model_dir in model_dirs:\n                new_cluster = []\n                rec = pycolmap.Reconstruction(model_dir)\n                cluster_images = [im.name for im in rec.images.values()]\n                new_cluster.append(sorted(cluster_images))\n                new_clusters.append(sorted(cluster_images))\n            self.clusters[cluster_idx] = new_clusters[0]\n            self.clusters.extend(new_clusters[1:])\n        return model_dirs[0] if model_dirs else None\n\n    def _run_colmap_with_all_images(self):\n        \"\"\"IGnores clusters and runs COLMAP on all images together. Calculates clusters based on the results of the COLMAP, as defined by the directories inside the sparse output folder.\"\"\"\n        cluster_ws, db_path, sparse_path, img_list_path = self._cluster_workspace(\"all\")\n        self._write_image_list(self.image_names, img_list_path)\n        pycolmap.extract_features(\n            database_path=db_path,\n            image_path=self.dataset_path,\n            image_names=self.image_names,\n            camera_mode=pycolmap.CameraMode.AUTO,\n            # sift_options=pycolmap.SiftExtractionOptions(),\n            extraction_options = pycolmap.FeatureExtractionOptions(\n                \n            ),\n            reader_options=pycolmap.ImageReaderOptions(\n                camera_model=\"SIMPLE_RADIAL\"\n            ),\n            # device=\"CPU\",\n        )\n        pycolmap.match_exhaustive(\n        database_path=db_path,\n        # sift_options=pycolmap.SiftMatchingOptions()\n        )\n\n        opts = pycolmap.IncrementalPipelineOptions()\n\n        opts.min_num_matches = 8\n        opts.mapper.init_min_num_inliers = 15\n        opts.mapper.init_max_error = 12\n        # opts.init_min_tri_angle = 10.0\n        \n        maps = pycolmap.incremental_mapping(\n            database_path=db_path,\n            image_path=self.dataset_path,\n            output_path=sparse_path,\n            options=opts,\n        )\n\n        model_dirs = [\n            os.path.join(sparse_path, d)\n            for d in os.listdir(sparse_path)\n            if os.path.isdir(os.path.join(sparse_path, d))\n        ]\n        \n        # Determine clusters based on which images are in which model directories\n        self.clusters = []\n        for model_dir in model_dirs:\n            rec = pycolmap.Reconstruction(model_dir)\n            cluster_images = [im.name for im in rec.images.values()]\n            self.clusters.append(sorted(cluster_images))\n            \n        return model_dirs\n\n    def _mat_to_str(self, mat):\n        return \";\".join(f\"{x:.6f}\" for x in mat.flatten())\n\n    def _extract_cluster_poses(self, model_dir, dataset_name, scene_name):\n        rec = pycolmap.Reconstruction(model_dir)\n        rows = []\n\n        for im in rec.images.values():\n            R, t = self._extract_pose_from_image(im)\n            rows.append({\n                \"image_id\": f'{dataset_name}_{scene_name}_{im.name}',\n                \"dataset\": dataset_name,\n                \"scene\": scene_name,\n                \"image\": im.name,\n                \"rotation_matrix\": self._mat_to_str(R),\n                \"translation_vector\": self._mat_to_str(t),\n            })\n\n        return pd.DataFrame(rows).sort_values(\"image\").reset_index(drop=True)\n\n    def build_submission(self, output_path=\"submission.csv\"):\n        if self.combined_pose_df is None or len(self.combined_pose_df) == 0:\n            raise RuntimeError(\"Run the pipeline first.\")\n\n        submission = self.combined_pose_df[[\n            \"image_id\", \n            \"dataset\",\n            \"scene\",\n            \"image\",\n            \"rotation_matrix\",\n            \"translation_vector\",\n        ]].copy()\n\n        submission.to_csv(output_path, index=False)\n        return submission\n\n    def run_full_pipeline(self):\n        self.extract_features()\n        self.pairwise_match()\n        self.ransac_filter()\n        self.prune_inliers()\n        self.cluster_images()\n        self.env = self._prepare_env()\n        colmap = self._run_colmap_with_own_features if self.use_custom_features else self._run_colmap_for_cluster\n\n        all_images_in_clusters = set(img for cluster in self.clusters for img in cluster)\n        outliers = [n for n in self.image_names if n not in all_images_in_clusters]\n        self.cluster_pose_dfs = []\n        for cluster_idx, cluster in enumerate(self.clusters):\n            if len(cluster)<2:\n                outliers.extend(cluster)\n                continue\n            model_dir = colmap(cluster_idx, cluster)\n            if model_dir is None:\n                outliers.extend(cluster)\n                continue\n\n            pred_df = self._extract_cluster_poses(\n                model_dir=model_dir,\n                dataset_name=os.path.basename(self.dataset_path),\n                scene_name=f\"cluster_{cluster_idx}\",\n            )\n            self.cluster_pose_dfs.append(pred_df)\n            \n        # Add outliers with nan poses and rotations\n        outlier_df = pd.DataFrame({\n            \"image_id\": [f'{os.path.basename(self.dataset_path)}_outliers_{i}_{name}' for i, name in enumerate(outliers)],\n            \"dataset\": [os.path.basename(self.dataset_path)] * len(outliers),\n            \"scene\": [f\"outliers\" for i in range(len(outliers))],\n            \"image\": outliers,\n            \"rotation_matrix\": [self._mat_to_str(np.array([np.nan] * 9))] * len(outliers),\n            \"translation_vector\": [self._mat_to_str(np.array([np.nan]*3))] * len(outliers)\n        })\n        self.cluster_pose_dfs.append(outlier_df)\n\n        self.combined_pose_df = (\n            pd.concat(self.cluster_pose_dfs, ignore_index=True)\n            if self.cluster_pose_dfs\n            else pd.DataFrame(columns=[\"image_id\", \"dataset\", \"scene\", \"image\", \"rotation_matrix\", \"translation_vector\"])\n        )\n        return self.combined_pose_df        \n\n    def run(self):\n        self.extract_features()\n        self.pairwise_match()\n        self.ransac_filter()\n        self.prune_inliers()\n        self.cluster_images()\n        \n        if self.train:\n            self.evaluate_clusters()\n        self.visualize_matches()\n        \n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.092992Z","iopub.execute_input":"2026-06-28T15:00:49.094054Z","iopub.status.idle":"2026-06-28T15:00:49.194879Z","shell.execute_reply.started":"2026-06-28T15:00:49.094021Z","shell.execute_reply":"2026-06-28T15:00:49.193974Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Επιλογή πηγής matches για το COLMAP. False = SIFT (όπως πριν). True = SuperPoint/LightGlue.\nUSE_RETRIEVAL_MATCHES = False\n\ndef _run_colmap_with_retrieval_matches(self, cluster_idx, cluster):\n    retr = self.imageretrieval\n    if not hasattr(retr, \"keypoints_xy\") or not hasattr(retr, \"pair_matches\"):\n        raise RuntimeError(\"colmap_source='retrieval' needs ImageRetrievalSuperPoint.\")\n\n    cluster_ws, db_path, sparse_path, img_list_path = self._cluster_workspace(cluster_idx)\n    self._write_image_list(cluster, img_list_path)\n    db = pycolmap.Database.open(db_path)\n\n    image_name_to_id = {}\n    for i, name in enumerate(cluster):\n        kps = retr.keypoints_xy.get(name)\n        if kps is None or len(kps) == 0:\n            continue\n        h, w = retr.image_hw[name]\n        f = 1.2 * max(w, h)\n        cam = pycolmap.Camera.create_from_model_id(\n            i, pycolmap.CameraModelId.SIMPLE_RADIAL, float(f), int(w), int(h))\n        cam_id = db.write_camera(cam)\n        img_id = db.write_image(pycolmap.Image(name=name, camera_id=cam_id))\n        image_name_to_id[name] = img_id\n        db.write_keypoints(img_id, np.ascontiguousarray(kps, dtype=np.float32))\n\n    cset = set(cluster)\n    added = 0\n    for (a, b), m in retr.pair_matches.items():\n        if a in cset and b in cset and a in image_name_to_id and b in image_name_to_id and m is not None and len(m):\n            db.write_matches(image_name_to_id[a], image_name_to_id[b],\n                             np.ascontiguousarray(m, dtype=np.uint32))\n            added += 1\n    print(f\"[retrieval-matches] cluster {cluster_idx}: {len(image_name_to_id)} imgs, {added} pairs, \"\n          f\"keypoints={db.num_keypoints()}\")\n\n    pycolmap.geometric_verification(db_path)\n    print(\"two view geometries:\", [len(x) for x in db.read_two_view_geometries()])\n\n    opts = pycolmap.IncrementalPipelineOptions()\n    opts.min_num_matches = 8\n    opts.mapper.init_min_num_inliers = 15\n    opts.mapper.init_max_error = 12\n    maps = pycolmap.incremental_mapping(database_path=db_path, image_path=self.dataset_path,\n                                        output_path=sparse_path, options=opts)\n    model_dirs = [os.path.join(sparse_path, d) for d in os.listdir(sparse_path)\n                  if os.path.isdir(os.path.join(sparse_path, d))]\n    db.close()\n    if len(model_dirs) > 1:\n        new_clusters = []\n        for md in model_dirs:\n            rec = pycolmap.Reconstruction(md)\n            new_clusters.append(sorted([im.name for im in rec.images.values()]))\n        self.clusters[cluster_idx] = new_clusters[0]\n        self.clusters.extend(new_clusters[1:])\n    return model_dirs[0] if model_dirs else None\n\nImageMatchingDataset._run_colmap_with_retrieval_matches = _run_colmap_with_retrieval_matches\n\n\ndef run_full_pipeline(self):\n    self.extract_features(); self.pairwise_match(); self.ransac_filter()\n    self.prune_inliers(); self.cluster_images()\n    self.env = self._prepare_env()\n\n    use_retrieval = globals().get(\"USE_RETRIEVAL_MATCHES\", False)\n    if self.use_custom_features:\n        primary = self._run_colmap_with_own_features\n    elif use_retrieval:\n        primary = self._run_colmap_with_retrieval_matches\n    else:\n        primary = self._run_colmap_for_cluster\n\n    all_in = set(img for c in self.clusters for img in c)\n    outliers = [n for n in self.image_names if n not in all_in]\n    self.cluster_pose_dfs = []\n    for cluster_idx, cluster in enumerate(self.clusters):\n        if len(cluster) < 2:\n            outliers.extend(cluster); continue\n        try:\n            model_dir = primary(cluster_idx, cluster)\n        except Exception as e:\n            print(f\"  [fallback] cluster {cluster_idx} retrieval-matches failed ({repr(e)}); using SIFT\")\n            try:\n                model_dir = self._run_colmap_for_cluster(cluster_idx, cluster)\n            except Exception as e2:\n                print(f\"  [fallback failed] {repr(e2)}\"); model_dir = None\n        if model_dir is None:\n            outliers.extend(cluster); continue\n        self.cluster_pose_dfs.append(self._extract_cluster_poses(\n            model_dir=model_dir, dataset_name=os.path.basename(self.dataset_path),\n            scene_name=f\"cluster_{cluster_idx}\"))\n\n    outlier_df = pd.DataFrame({\n        \"image_id\": [f'{os.path.basename(self.dataset_path)}_outliers_{i}_{n}' for i, n in enumerate(outliers)],\n        \"dataset\": [os.path.basename(self.dataset_path)] * len(outliers),\n        \"scene\": [\"outliers\"] * len(outliers), \"image\": outliers,\n        \"rotation_matrix\": [self._mat_to_str(np.array([np.nan]*9))] * len(outliers),\n        \"translation_vector\": [self._mat_to_str(np.array([np.nan]*3))] * len(outliers)})\n    self.cluster_pose_dfs.append(outlier_df)\n    self.combined_pose_df = (pd.concat(self.cluster_pose_dfs, ignore_index=True)\n        if self.cluster_pose_dfs else pd.DataFrame(\n            columns=[\"image_id\",\"dataset\",\"scene\",\"image\",\"rotation_matrix\",\"translation_vector\"]))\n    return self.combined_pose_df\n\nImageMatchingDataset.run_full_pipeline = run_full_pipeline","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.196317Z","iopub.execute_input":"2026-06-28T15:00:49.196668Z","iopub.status.idle":"2026-06-28T15:00:49.218027Z","shell.execute_reply.started":"2026-06-28T15:00:49.196620Z","shell.execute_reply":"2026-06-28T15:00:49.217112Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# ALIKE MATCHER","metadata":{}},{"cell_type":"code","source":"import os, cv2, numpy as np, torch\nfrom lightglue import LightGlue, ALIKED\nfrom lightglue.utils import numpy_image_to_torch, rbd\n\nUSE_ALIKE_MATCHES = False   # True (μαζί με USE_RETRIEVAL_MATCHES=True) → προσθέτει ALIKE\n\nclass ALIKEDLightGlueMatcher:\n    def __init__(self, args=None):\n        args = args or {}\n        self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        self.resize_long       = args.get(\"resize_long\", 1280)\n        self.max_num_keypoints = args.get(\"max_num_keypoints\", 2048)\n        self.downscale_only    = args.get(\"downscale_only\", True)\n        self.extractor = ALIKED(max_num_keypoints=self.max_num_keypoints).eval().to(self.device)\n        self.matcher   = LightGlue(features=\"aliked\").eval().to(self.device)\n        self.feats, self.keypoints_xy, self.image_hw, self.pair_matches = {}, {}, {}, {}\n\n    def _to_tensor(self, img_bgr):\n        img = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)\n        h, w = img.shape[:2]; long_side = max(h, w)\n        need = (long_side > self.resize_long) or (not self.downscale_only and long_side != self.resize_long)\n        if need:\n            s = self.resize_long / long_side\n            nw, nh = max(1, int(round(w*s))), max(1, int(round(h*s)))\n            img = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_AREA if s < 1 else cv2.INTER_CUBIC)\n        return numpy_image_to_torch(img).to(self.device)\n\n    def setup(self, load_fn, img_names):\n        with torch.inference_mode():\n            for name in img_names:\n                img = load_fn(name)\n                if img is None: continue\n                t = self._to_tensor(img)\n                f = self.extractor.extract(t)\n                self.feats[name] = f\n                self.keypoints_xy[name] = f[\"keypoints\"][0].detach().cpu().numpy().astype(np.float32)\n                self.image_hw[name] = (int(t.shape[-2]), int(t.shape[-1]))\n\n    def match_pairs(self, pairs):\n        with torch.inference_mode():\n            for (a, b) in pairs:\n                fa, fb = self.feats.get(a), self.feats.get(b)\n                if fa is None or fb is None: continue\n                m = rbd(self.matcher({\"image0\": fa, \"image1\": fb}))[\"matches\"]\n                if m.shape[0] > 0:\n                    self.pair_matches[(a, b)] = m.detach().cpu().numpy().astype(np.uint32)\n        self.feats = {}   # ελευθέρωσε GPU μνήμη μετά το matching\n\n\ndef _combine_match_sources(sources, image_names, pairs):\n    combined_kps, offsets = {}, {}\n    for name in image_names:\n        parts, offs, cur = [], [], 0\n        for s in sources:\n            offs.append(cur)\n            kp = s.keypoints_xy.get(name)\n            if kp is not None and len(kp): parts.append(kp); cur += len(kp)\n        offsets[name] = offs\n        combined_kps[name] = (np.concatenate(parts, 0).astype(np.float32)\n                              if parts else np.zeros((0, 2), np.float32))\n    combined_pm = {}\n    for (a, b) in pairs:\n        chunks = []\n        for i, s in enumerate(sources):\n            m = s.pair_matches.get((a, b))\n            if m is None or len(m) == 0: continue\n            mm = m.astype(np.int64).copy()\n            mm[:, 0] += offsets[a][i]; mm[:, 1] += offsets[b][i]\n            chunks.append(mm)\n        if chunks:\n            combined_pm[(a, b)] = np.concatenate(chunks, 0).astype(np.uint32)\n    return combined_kps, combined_pm\n\n\ndef _run_colmap_with_retrieval_matches(self, cluster_idx, cluster):\n    kps_src = getattr(self, \"_combined_keypoints\", None) or self.imageretrieval.keypoints_xy\n    pm_src  = getattr(self, \"_combined_pair_matches\", None) or self.imageretrieval.pair_matches\n    hw_src  = getattr(self, \"_combined_image_hw\", None) or self.imageretrieval.image_hw\n\n    cluster_ws, db_path, sparse_path, img_list_path = self._cluster_workspace(cluster_idx)\n    self._write_image_list(cluster, img_list_path)\n    db = pycolmap.Database.open(db_path)\n\n    name_to_id = {}\n    for i, name in enumerate(cluster):\n        kps = kps_src.get(name)\n        if kps is None or len(kps) == 0: continue\n        h, w = hw_src[name]; f = 1.2 * max(w, h)\n        cam = pycolmap.Camera.create_from_model_id(i, pycolmap.CameraModelId.SIMPLE_RADIAL, float(f), int(w), int(h))\n        cam_id = db.write_camera(cam)\n        img_id = db.write_image(pycolmap.Image(name=name, camera_id=cam_id))\n        name_to_id[name] = img_id\n        db.write_keypoints(img_id, np.ascontiguousarray(kps, dtype=np.float32))\n\n    cset, added = set(cluster), 0\n    for (a, b), m in pm_src.items():\n        if a in cset and b in cset and a in name_to_id and b in name_to_id and m is not None and len(m):\n            db.write_matches(name_to_id[a], name_to_id[b], np.ascontiguousarray(m, dtype=np.uint32))\n            added += 1\n    print(f\"[ensemble-matches] cluster {cluster_idx}: {len(name_to_id)} imgs, {added} pairs, keypoints={db.num_keypoints()}\")\n\n    pycolmap.geometric_verification(db_path)\n    print(\"two view geometries:\", [len(x) for x in db.read_two_view_geometries()])\n    opts = pycolmap.IncrementalPipelineOptions()\n    opts.min_num_matches = 8; opts.mapper.init_min_num_inliers = 15; opts.mapper.init_max_error = 12\n    pycolmap.incremental_mapping(database_path=db_path, image_path=self.dataset_path, output_path=sparse_path, options=opts)\n    model_dirs = [os.path.join(sparse_path, d) for d in os.listdir(sparse_path)\n                  if os.path.isdir(os.path.join(sparse_path, d))]\n    db.close()\n    if len(model_dirs) > 1:\n        new = [sorted([im.name for im in pycolmap.Reconstruction(md).images.values()]) for md in model_dirs]\n        self.clusters[cluster_idx] = new[0]; self.clusters.extend(new[1:])\n    return model_dirs[0] if model_dirs else None\n\nImageMatchingDataset._run_colmap_with_retrieval_matches = _run_colmap_with_retrieval_matches\n\n\ndef run_full_pipeline(self):\n    self.extract_features(); self.pairwise_match(); self.ransac_filter()\n    self.prune_inliers(); self.cluster_images()\n    self.env = self._prepare_env()\n    use_retrieval = globals().get(\"USE_RETRIEVAL_MATCHES\", False)\n    use_alike     = globals().get(\"USE_ALIKE_MATCHES\", False)\n\n    self._combined_keypoints = self._combined_pair_matches = self._combined_image_hw = None\n    if use_retrieval and not self.use_custom_features:\n        retr = self.imageretrieval\n        sources = [retr]\n        if use_alike:\n            try:\n                alike = ALIKEDLightGlueMatcher({\"resize_long\": retr.resize_long,\n                                                \"max_num_keypoints\": retr.max_num_keypoints})\n                alike.setup(lambda n: cv2.imread(os.path.join(self.dataset_path, n), cv2.IMREAD_COLOR),\n                            list(retr.keypoints_xy.keys()))\n                alike.match_pairs(list(retr.pair_matches.keys()))\n                sources.append(alike)\n                print(f\"[ALIKE] imgs={len(alike.keypoints_xy)}, matched pairs={len(alike.pair_matches)}\")\n            except Exception as e:\n                print(f\"[ALIKE] disabled (failed: {repr(e)}); SuperPoint only\")\n        ck, cpm = _combine_match_sources(sources, list(retr.keypoints_xy.keys()),\n                                         list(retr.pair_matches.keys()))\n        self._combined_keypoints, self._combined_pair_matches, self._combined_image_hw = ck, cpm, retr.image_hw\n\n    if self.use_custom_features:   primary = self._run_colmap_with_own_features\n    elif use_retrieval:            primary = self._run_colmap_with_retrieval_matches\n    else:                          primary = self._run_colmap_for_cluster\n\n    all_in = set(img for c in self.clusters for img in c)\n    outliers = [n for n in self.image_names if n not in all_in]\n    self.cluster_pose_dfs = []\n    for ci, cluster in enumerate(self.clusters):\n        if len(cluster) < 2: outliers.extend(cluster); continue\n        try:\n            md = primary(ci, cluster)\n        except Exception as e:\n            print(f\"  [fallback] cluster {ci} failed ({repr(e)}); SIFT\"); \n            try: md = self._run_colmap_for_cluster(ci, cluster)\n            except Exception as e2: print(f\"  [fallback failed] {repr(e2)}\"); md = None\n        if md is None: outliers.extend(cluster); continue\n        self.cluster_pose_dfs.append(self._extract_cluster_poses(\n            model_dir=md, dataset_name=os.path.basename(self.dataset_path), scene_name=f\"cluster_{ci}\"))\n\n    outlier_df = pd.DataFrame({\n        \"image_id\": [f'{os.path.basename(self.dataset_path)}_outliers_{i}_{n}' for i, n in enumerate(outliers)],\n        \"dataset\": [os.path.basename(self.dataset_path)] * len(outliers),\n        \"scene\": [\"outliers\"] * len(outliers), \"image\": outliers,\n        \"rotation_matrix\": [self._mat_to_str(np.array([np.nan]*9))] * len(outliers),\n        \"translation_vector\": [self._mat_to_str(np.array([np.nan]*3))] * len(outliers)})\n    self.cluster_pose_dfs.append(outlier_df)\n    self.combined_pose_df = (pd.concat(self.cluster_pose_dfs, ignore_index=True)\n        if self.cluster_pose_dfs else pd.DataFrame(\n            columns=[\"image_id\",\"dataset\",\"scene\",\"image\",\"rotation_matrix\",\"translation_vector\"]))\n    return self.combined_pose_df\n\nImageMatchingDataset.run_full_pipeline = run_full_pipeline","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.219387Z","iopub.execute_input":"2026-06-28T15:00:49.219798Z","iopub.status.idle":"2026-06-28T15:00:49.260514Z","shell.execute_reply.started":"2026-06-28T15:00:49.219758Z","shell.execute_reply":"2026-06-28T15:00:49.259526Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# SIFT+NN matcher","metadata":{}},{"cell_type":"code","source":"import os, cv2, numpy as np\n\nUSE_SIFTNN_MATCHES = False   # True (μαζί με USE_RETRIEVAL_MATCHES=True) → προσθέτει SIFT+NN\n\nclass SIFTNNMatcher:\n    def __init__(self, args=None):\n        args = args or {}\n        self.resize_long    = args.get(\"resize_long\", 1280)\n        self.nfeatures      = args.get(\"nfeatures\", 8192)\n        self.ratio          = args.get(\"ratio\", 0.85)\n        self.downscale_only = args.get(\"downscale_only\", True)\n        self.rootsift       = args.get(\"rootsift\", True)\n        self.mutual         = args.get(\"mutual\", True)\n        self.sift = cv2.SIFT_create(nfeatures=self.nfeatures)\n        self.bf   = cv2.BFMatcher(cv2.NORM_L2)\n        self.keypoints_xy, self.descriptors, self.image_hw, self.pair_matches = {}, {}, {}, {}\n\n    def _resize(self, img_bgr):\n        h, w = img_bgr.shape[:2]; long_side = max(h, w)\n        need = (long_side > self.resize_long) or (not self.downscale_only and long_side != self.resize_long)\n        if need:\n            s = self.resize_long / long_side\n            nw, nh = max(1, int(round(w*s))), max(1, int(round(h*s)))\n            img_bgr = cv2.resize(img_bgr, (nw, nh), interpolation=cv2.INTER_AREA if s < 1 else cv2.INTER_CUBIC)\n        return img_bgr\n\n    def setup(self, load_fn, img_names):\n        for name in img_names:\n            img = load_fn(name)\n            if img is None: continue\n            img = self._resize(img)\n            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n            kps, desc = self.sift.detectAndCompute(gray, None)\n            self.image_hw[name] = (gray.shape[0], gray.shape[1])\n            if desc is None or len(kps) == 0:\n                self.keypoints_xy[name] = np.zeros((0, 2), np.float32); self.descriptors[name] = None; continue\n            if self.rootsift:\n                desc = desc / (desc.sum(axis=1, keepdims=True) + 1e-7)\n                desc = np.sqrt(desc)\n            self.keypoints_xy[name] = np.array([kp.pt for kp in kps], dtype=np.float32)\n            self.descriptors[name]  = desc.astype(np.float32)\n\n    def _ratio(self, d1, d2):\n        out = []\n        for pr in self.bf.knnMatch(d1, d2, k=2):\n            if len(pr) < 2: continue\n            m, n = pr\n            if m.distance < self.ratio * n.distance:\n                out.append((m.queryIdx, m.trainIdx))\n        return out\n\n    def match_pairs(self, pairs):\n        for (a, b) in pairs:\n            da, db = self.descriptors.get(a), self.descriptors.get(b)\n            if da is None or db is None or len(da) < 2 or len(db) < 2: continue\n            m_ab = self._ratio(da, db)\n            if self.mutual:\n                set_ba = set(self._ratio(db, da))            # (j, i) με j∈b, i∈a\n                m = [(i, j) for (i, j) in m_ab if (j, i) in set_ba]\n            else:\n                m = m_ab\n            if m:\n                self.pair_matches[(a, b)] = np.array(m, dtype=np.uint32)\n        self.descriptors = {}   # ελευθέρωσε μνήμη\n\n\ndef _build_extra_matchers(self):\n    \"\"\"Χτίζει τους extra matchers με βάση τα global flags. Νέοι matchers → μπαίνουν ΕΔΩ.\"\"\"\n    retr = self.imageretrieval\n    load_fn = lambda n: cv2.imread(os.path.join(self.dataset_path, n), cv2.IMREAD_COLOR)\n    names = list(retr.keypoints_xy.keys())\n    pairs = list(retr.pair_matches.keys())\n    extras = []\n    if globals().get(\"USE_ALIKE_MATCHES\", False):\n        try:\n            al = ALIKEDLightGlueMatcher({\"resize_long\": retr.resize_long, \"max_num_keypoints\": retr.max_num_keypoints})\n            al.setup(load_fn, names); al.match_pairs(pairs); extras.append(al)\n            print(f\"[ALIKE] imgs={len(al.keypoints_xy)}, pairs={len(al.pair_matches)}\")\n        except Exception as e:\n            print(f\"[ALIKE] disabled: {repr(e)}\")\n    if globals().get(\"USE_SIFTNN_MATCHES\", False):\n        try:\n            sn = SIFTNNMatcher({\"resize_long\": retr.resize_long})\n            sn.setup(load_fn, names); sn.match_pairs(pairs); extras.append(sn)\n            print(f\"[SIFT-NN] imgs={len(sn.keypoints_xy)}, pairs={len(sn.pair_matches)}\")\n        except Exception as e:\n            print(f\"[SIFT-NN] disabled: {repr(e)}\")\n    return extras\nImageMatchingDataset._build_extra_matchers = _build_extra_matchers\n\n\ndef run_full_pipeline(self):\n    self.extract_features(); self.pairwise_match(); self.ransac_filter()\n    self.prune_inliers(); self.cluster_images()\n    self.env = self._prepare_env()\n    use_retrieval = globals().get(\"USE_RETRIEVAL_MATCHES\", False)\n\n    self._combined_keypoints = self._combined_pair_matches = self._combined_image_hw = None\n    if use_retrieval and not self.use_custom_features:\n        retr = self.imageretrieval\n        sources = [retr] + self._build_extra_matchers()\n        ck, cpm = _combine_match_sources(sources, list(retr.keypoints_xy.keys()), list(retr.pair_matches.keys()))\n        self._combined_keypoints, self._combined_pair_matches, self._combined_image_hw = ck, cpm, retr.image_hw\n\n    if self.use_custom_features:   primary = self._run_colmap_with_own_features\n    elif use_retrieval:            primary = self._run_colmap_with_retrieval_matches\n    else:                          primary = self._run_colmap_for_cluster\n\n    all_in = set(img for c in self.clusters for img in c)\n    outliers = [n for n in self.image_names if n not in all_in]\n    self.cluster_pose_dfs = []\n    for ci, cluster in enumerate(self.clusters):\n        if len(cluster) < 2: outliers.extend(cluster); continue\n        try:\n            md = primary(ci, cluster)\n        except Exception as e:\n            print(f\"  [fallback] cluster {ci} failed ({repr(e)}); SIFT\")\n            try: md = self._run_colmap_for_cluster(ci, cluster)\n            except Exception as e2: print(f\"  [fallback failed] {repr(e2)}\"); md = None\n        if md is None: outliers.extend(cluster); continue\n        self.cluster_pose_dfs.append(self._extract_cluster_poses(\n            model_dir=md, dataset_name=os.path.basename(self.dataset_path), scene_name=f\"cluster_{ci}\"))\n\n    outlier_df = pd.DataFrame({\n        \"image_id\": [f'{os.path.basename(self.dataset_path)}_outliers_{i}_{n}' for i, n in enumerate(outliers)],\n        \"dataset\": [os.path.basename(self.dataset_path)] * len(outliers),\n        \"scene\": [\"outliers\"] * len(outliers), \"image\": outliers,\n        \"rotation_matrix\": [self._mat_to_str(np.array([np.nan]*9))] * len(outliers),\n        \"translation_vector\": [self._mat_to_str(np.array([np.nan]*3))] * len(outliers)})\n    self.cluster_pose_dfs.append(outlier_df)\n    self.combined_pose_df = (pd.concat(self.cluster_pose_dfs, ignore_index=True)\n        if self.cluster_pose_dfs else pd.DataFrame(\n            columns=[\"image_id\",\"dataset\",\"scene\",\"image\",\"rotation_matrix\",\"translation_vector\"]))\n    return self.combined_pose_df\n\nImageMatchingDataset.run_full_pipeline = run_full_pipeline","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.261902Z","iopub.execute_input":"2026-06-28T15:00:49.262380Z","iopub.status.idle":"2026-06-28T15:00:49.295369Z","shell.execute_reply.started":"2026-06-28T15:00:49.262289Z","shell.execute_reply":"2026-06-28T15:00:49.294553Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Ransac & Ensemble","metadata":{}},{"cell_type":"code","source":"import cv2, numpy as np\n\nUSE_ENSEMBLE_RANSAC  = False   # True → ενεργοποιεί το φίλτρο 150\nENSEMBLE_MIN_INLIERS = 150     # κράτα ζεύγος μόνο αν έχει ≥ τόσα inliers\nENSEMBLE_RANSAC_THR  = 1.0     # κατώφλι RANSAC (pixels, στο 1280 frame)\n\n\ndef _ransac_filter_combined(keypoints, pair_matches, min_inliers=150, thr=1.0, conf=0.9999):\n    out, kept, dropped = {}, 0, 0\n    for (a, b), m in pair_matches.items():\n        if m is None or len(m) < 8:\n            dropped += 1; continue\n        ka, kb = keypoints.get(a), keypoints.get(b)\n        if ka is None or kb is None or len(ka) == 0 or len(kb) == 0:\n            dropped += 1; continue\n        pts_a = ka[m[:, 0]].astype(np.float32)\n        pts_b = kb[m[:, 1]].astype(np.float32)\n        F, mask = cv2.findFundamentalMat(pts_a, pts_b, cv2.USAC_MAGSAC, thr, conf, 100000)\n        if F is None or mask is None:\n            dropped += 1; continue\n        inliers = m[mask.ravel().astype(bool)]\n        if len(inliers) >= min_inliers:\n            out[(a, b)] = inliers.astype(np.uint32); kept += 1\n        else:\n            dropped += 1\n    print(f\"[ensemble-RANSAC] kept {kept} pairs (>= {min_inliers} inliers), dropped {dropped}\")\n    return out\n\n\ndef _postprocess_combined(self, ck, cpm):\n    \"\"\"Hook μετά το combine. Νέα post-processing (π.χ. crops) μπαίνουν ΕΔΩ.\"\"\"\n    if globals().get(\"USE_ENSEMBLE_RANSAC\", False):\n        cpm = _ransac_filter_combined(\n            ck, cpm,\n            min_inliers=globals().get(\"ENSEMBLE_MIN_INLIERS\", 150),\n            thr=globals().get(\"ENSEMBLE_RANSAC_THR\", 1.0))\n    return ck, cpm\nImageMatchingDataset._postprocess_combined = _postprocess_combined\n\n\ndef run_full_pipeline(self):\n    self.extract_features(); self.pairwise_match(); self.ransac_filter()\n    self.prune_inliers(); self.cluster_images()\n    self.env = self._prepare_env()\n    use_retrieval = globals().get(\"USE_RETRIEVAL_MATCHES\", False)\n\n    self._combined_keypoints = self._combined_pair_matches = self._combined_image_hw = None\n    if use_retrieval and not self.use_custom_features:\n        retr = self.imageretrieval\n        sources = [retr] + self._build_extra_matchers()\n        ck, cpm = _combine_match_sources(sources, list(retr.keypoints_xy.keys()), list(retr.pair_matches.keys()))\n        ck, cpm = self._postprocess_combined(ck, cpm)          # <-- ΝΕΟ: ensemble RANSAC + φίλτρο 150\n        self._combined_keypoints, self._combined_pair_matches, self._combined_image_hw = ck, cpm, retr.image_hw\n\n    if self.use_custom_features:   primary = self._run_colmap_with_own_features\n    elif use_retrieval:            primary = self._run_colmap_with_retrieval_matches\n    else:                          primary = self._run_colmap_for_cluster\n\n    all_in = set(img for c in self.clusters for img in c)\n    outliers = [n for n in self.image_names if n not in all_in]\n    self.cluster_pose_dfs = []\n    for ci, cluster in enumerate(self.clusters):\n        if len(cluster) < 2: outliers.extend(cluster); continue\n        try:\n            md = primary(ci, cluster)\n        except Exception as e:\n            print(f\"  [fallback] cluster {ci} failed ({repr(e)}); SIFT\")\n            try: md = self._run_colmap_for_cluster(ci, cluster)\n            except Exception as e2: print(f\"  [fallback failed] {repr(e2)}\"); md = None\n        if md is None: outliers.extend(cluster); continue\n        self.cluster_pose_dfs.append(self._extract_cluster_poses(\n            model_dir=md, dataset_name=os.path.basename(self.dataset_path), scene_name=f\"cluster_{ci}\"))\n\n    outlier_df = pd.DataFrame({\n        \"image_id\": [f'{os.path.basename(self.dataset_path)}_outliers_{i}_{n}' for i, n in enumerate(outliers)],\n        \"dataset\": [os.path.basename(self.dataset_path)] * len(outliers),\n        \"scene\": [\"outliers\"] * len(outliers), \"image\": outliers,\n        \"rotation_matrix\": [self._mat_to_str(np.array([np.nan]*9))] * len(outliers),\n        \"translation_vector\": [self._mat_to_str(np.array([np.nan]*3))] * len(outliers)})\n    self.cluster_pose_dfs.append(outlier_df)\n    self.combined_pose_df = (pd.concat(self.cluster_pose_dfs, ignore_index=True)\n        if self.cluster_pose_dfs else pd.DataFrame(\n            columns=[\"image_id\",\"dataset\",\"scene\",\"image\",\"rotation_matrix\",\"translation_vector\"]))\n    return self.combined_pose_df\n\nImageMatchingDataset.run_full_pipeline = run_full_pipeline","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.296801Z","iopub.execute_input":"2026-06-28T15:00:49.297174Z","iopub.status.idle":"2026-06-28T15:00:49.321656Z","shell.execute_reply.started":"2026-06-28T15:00:49.297131Z","shell.execute_reply":"2026-06-28T15:00:49.320820Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Cropping","metadata":{}},{"cell_type":"code","source":"import os, cv2, numpy as np, torch\nfrom lightglue import LightGlue, SuperPoint, ALIKED\nfrom lightglue.utils import numpy_image_to_torch, rbd\n\nUSE_CROP_MATCHES   = False   # True → ενεργοποιεί crops (βήματα 4-6)\nCROP_RESIZE_LONG   = 1280\nCROP_LO_PCT, CROP_HI_PCT = 5, 95\nCROP_MARGIN        = 0.15\nCROP_MIN_SRC       = 20      # ελάχιστα base matches για να οριστεί box\nCROP_MIN_BOX       = 32      # αγνόησε πολύ μικρά boxes (px)\n\n\ndef _resize_to_long(img_bgr, resize_long, downscale_only=True):\n    h, w = img_bgr.shape[:2]; long_side = max(h, w)\n    need = (long_side > resize_long) or (not downscale_only and long_side != resize_long)\n    if need:\n        s = resize_long / long_side\n        nw, nh = max(1, int(round(w*s))), max(1, int(round(h*s)))\n        img_bgr = cv2.resize(img_bgr, (nw, nh), interpolation=cv2.INTER_AREA if s < 1 else cv2.INTER_CUBIC)\n    return img_bgr\n\n\nclass CropMatcher:\n    def __init__(self, args=None):\n        args = args or {}\n        self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        self.crop_resize_long = args.get(\"crop_resize_long\", 1280)\n        self.max_num_keypoints = args.get(\"max_num_keypoints\", 2048)\n        self.lo_pct = args.get(\"lo_pct\", 5); self.hi_pct = args.get(\"hi_pct\", 95)\n        self.margin = args.get(\"margin\", 0.15)\n        self.min_src = args.get(\"min_src\", 20); self.min_box = args.get(\"min_box\", 32)\n        self.sp    = SuperPoint(max_num_keypoints=self.max_num_keypoints).eval().to(self.device)\n        self.sp_lg = LightGlue(features=\"superpoint\").eval().to(self.device)\n        self.al    = ALIKED(max_num_keypoints=self.max_num_keypoints).eval().to(self.device)\n        self.al_lg = LightGlue(features=\"aliked\").eval().to(self.device)\n\n    def _box(self, pts, W, H):\n        x_lo, x_hi = np.percentile(pts[:, 0], [self.lo_pct, self.hi_pct])\n        y_lo, y_hi = np.percentile(pts[:, 1], [self.lo_pct, self.hi_pct])\n        bw, bh = max(1.0, x_hi-x_lo), max(1.0, y_hi-y_lo)\n        x0 = int(max(0, x_lo - self.margin*bw)); x1 = int(min(W, x_hi + self.margin*bw))\n        y0 = int(max(0, y_lo - self.margin*bh)); y1 = int(min(H, y_hi + self.margin*bh))\n        return x0, y0, x1, y1\n\n    def _extract(self, extractor, crop_bgr):\n        rgb = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2RGB)\n        h, w = rgb.shape[:2]; s = self.crop_resize_long / max(h, w)\n        nw, nh = max(1, int(round(w*s))), max(1, int(round(h*s)))\n        rgb = cv2.resize(rgb, (nw, nh), interpolation=cv2.INTER_CUBIC if s > 1 else cv2.INTER_AREA)\n        t = numpy_image_to_torch(rgb).to(self.device)\n        return extractor.extract(t), s\n\n    def match_pair_on_crops(self, img_a, img_b, pts_a, pts_b):\n        Ha, Wa = img_a.shape[:2]; Hb, Wb = img_b.shape[:2]\n        if len(pts_a) < self.min_src or len(pts_b) < self.min_src: return None\n        xa0, ya0, xa1, ya1 = self._box(pts_a, Wa, Ha)\n        xb0, yb0, xb1, yb1 = self._box(pts_b, Wb, Hb)\n        if min(xa1-xa0, ya1-ya0, xb1-xb0, yb1-yb0) < self.min_box: return None\n        crop_a, crop_b = img_a[ya0:ya1, xa0:xa1], img_b[yb0:yb1, xb0:xb1]\n        if crop_a.size == 0 or crop_b.size == 0: return None\n        A_all, B_all = [], []\n        with torch.inference_mode():\n            for extractor, matcher in [(self.sp, self.sp_lg), (self.al, self.al_lg)]:\n                fa, sa = self._extract(extractor, crop_a)\n                fb, sb = self._extract(extractor, crop_b)\n                m = rbd(matcher({\"image0\": fa, \"image1\": fb}))[\"matches\"].detach().cpu().numpy()\n                if len(m) == 0: continue\n                ka = rbd(fa)[\"keypoints\"].detach().cpu().numpy()\n                kb = rbd(fb)[\"keypoints\"].detach().cpu().numpy()\n                ma, mb = ka[m[:, 0]], kb[m[:, 1]]\n                A_all.append(np.stack([xa0 + ma[:, 0]/sa, ya0 + ma[:, 1]/sa], 1))   # back to full frame\n                B_all.append(np.stack([xb0 + mb[:, 0]/sb, yb0 + mb[:, 1]/sb], 1))\n        if not A_all: return None\n        A = np.concatenate(A_all, 0).astype(np.float32)\n        B = np.concatenate(B_all, 0).astype(np.float32)\n        ml = np.stack([np.arange(len(A)), np.arange(len(A))], 1).astype(np.int64)\n        return A, B, ml\n\n\ndef _add_crop_matches(self, ck, cpm):\n    try:\n        cm = CropMatcher({\"crop_resize_long\": globals().get(\"CROP_RESIZE_LONG\", 1280),\n                          \"max_num_keypoints\": self.imageretrieval.max_num_keypoints,\n                          \"lo_pct\": globals().get(\"CROP_LO_PCT\", 5), \"hi_pct\": globals().get(\"CROP_HI_PCT\", 95),\n                          \"margin\": globals().get(\"CROP_MARGIN\", 0.15),\n                          \"min_src\": globals().get(\"CROP_MIN_SRC\", 20), \"min_box\": globals().get(\"CROP_MIN_BOX\", 32)})\n    except Exception as e:\n        print(f\"[crops] disabled (init failed: {repr(e)})\"); return ck, cpm\n\n    resize_long = self.imageretrieval.resize_long\n    names = set(a for p in cpm for a in p)\n    imgs = {}\n    for n in names:\n        im = cv2.imread(os.path.join(self.dataset_path, n), cv2.IMREAD_COLOR)\n        if im is not None: imgs[n] = _resize_to_long(im, resize_long, True)\n\n    ck = {k: (v.copy() if v is not None else np.zeros((0, 2), np.float32)) for k, v in ck.items()}\n    added_pairs, added_pts = 0, 0\n    for (a, b), m in list(cpm.items()):\n        if a not in imgs or b not in imgs or m is None or len(m) == 0: continue\n        try:\n            res = cm.match_pair_on_crops(imgs[a], imgs[b], ck[a][m[:, 0]], ck[b][m[:, 1]])\n        except Exception:\n            continue\n        if res is None: continue\n        A, B, ml = res\n        if len(ml) == 0: continue\n        off_a, off_b = len(ck[a]), len(ck[b])\n        ck[a] = np.concatenate([ck[a], A], 0); ck[b] = np.concatenate([ck[b], B], 0)\n        ml = ml.copy(); ml[:, 0] += off_a; ml[:, 1] += off_b\n        cpm[(a, b)] = np.concatenate([cpm[(a, b)], ml.astype(np.uint32)], 0)\n        added_pairs += 1; added_pts += len(ml)\n    print(f\"[crops] augmented {added_pairs} pairs, +{added_pts} crop matches\")\n    return ck, cpm\nImageMatchingDataset._add_crop_matches = _add_crop_matches\n\n\ndef _postprocess_combined(self, ck, cpm):\n    if globals().get(\"USE_CROP_MATCHES\", False):\n        ck, cpm = self._add_crop_matches(ck, cpm)          # βήματα 4-6\n    if globals().get(\"USE_ENSEMBLE_RANSAC\", False):\n        cpm = _ransac_filter_combined(ck, cpm,\n            min_inliers=globals().get(\"ENSEMBLE_MIN_INLIERS\", 150),\n            thr=globals().get(\"ENSEMBLE_RANSAC_THR\", 1.0))   # βήμα 7\n    return ck, cpm\nImageMatchingDataset._postprocess_combined = _postprocess_combined","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from itertools import product\nimport tqdm\n\ndef _scene_image_sets(dataset): # Construct a set that connects the scenes (of train set) with the right corresponding imaged\n    groups = {}\n    for scene, images in dataset.grouped_by_scene.items():\n        if scene == \"__unlabeled__\":\n            continue\n        groups[scene] = {img_info[\"image\"] for img_info in images}\n    return groups\n\ndef clustering_score_proxy(dataset, purity_weight=0.25): # Metrics calculation\n    \"\"\"\n    Reconstruction-aware clustering evaluation.\n\n    Primary metric:\n        scene_recall (coverage)\n\n    Secondary metric:\n        cluster_purity\n\n    Combined score:\n        scene_recall - purity_weight * (1 - cluster_purity)\n\n    Rationale:\n        Splitting a scene removes valid matches forever.\n        Merging scenes may still be recoverable by SfM.\n    \"\"\"\n\n    true_scenes = _scene_image_sets(dataset)\n    predicted_clusters = [set(cluster) for cluster in dataset.clusters]\n\n    # --------------------------------------------------\n    # Scene Recall / Coverage\n    # --------------------------------------------------\n\n    scene_details = []\n    total_scene_images = 0\n    recovered_scene_images = 0\n\n    for scene_name, scene_images in true_scenes.items():\n        scene_size = len(scene_images)\n\n        best_overlap = 0\n        best_cluster = None\n\n        for cluster in predicted_clusters:\n            overlap = len(scene_images & cluster)\n\n            if overlap > best_overlap:\n                best_overlap = overlap\n                best_cluster = cluster\n\n        coverage = (\n            best_overlap / scene_size\n            if scene_size > 0\n            else 0.0\n        )\n\n        scene_details.append(\n            {\n                \"scene\": scene_name,\n                \"scene_size\": scene_size,\n                \"best_overlap\": best_overlap,\n                \"coverage\": coverage,\n                \"best_cluster_size\":\n                    len(best_cluster) if best_cluster else 0,\n            }\n        )\n\n        total_scene_images += scene_size\n        recovered_scene_images += best_overlap\n\n    scene_recall = (\n        recovered_scene_images / total_scene_images\n        if total_scene_images\n        else 0.0\n    )\n\n    mean_scene_recall = (\n        sum(d[\"coverage\"] for d in scene_details) / len(scene_details)\n        if scene_details\n        else 0.0\n    )\n\n    # --------------------------------------------------\n    # Cluster Purity\n    # --------------------------------------------------\n\n    cluster_details = []\n    total_cluster_images = 0\n    pure_cluster_images = 0\n\n    for cluster_idx, cluster in enumerate(predicted_clusters):\n        if not cluster:\n            continue\n\n        dominant_scene = None\n        dominant_overlap = 0\n\n        for scene_name, scene_images in true_scenes.items():\n            overlap = len(cluster & scene_images)\n\n            if overlap > dominant_overlap:\n                dominant_overlap = overlap\n                dominant_scene = scene_name\n\n        purity = dominant_overlap / len(cluster)\n\n        cluster_details.append(\n            {\n                \"cluster\": cluster_idx,\n                \"cluster_size\": len(cluster),\n                \"dominant_scene\": dominant_scene,\n                \"dominant_overlap\": dominant_overlap,\n                \"purity\": purity,\n            }\n        )\n\n        total_cluster_images += len(cluster)\n        pure_cluster_images += dominant_overlap\n\n    cluster_purity = (\n        pure_cluster_images / total_cluster_images\n        if total_cluster_images\n        else 0.0\n    )\n\n    mean_cluster_purity = (\n        sum(d[\"purity\"] for d in cluster_details) / len(cluster_details)\n        if cluster_details\n        else 0.0\n    )\n\n    # --------------------------------------------------\n    # Reconstruction-aware score\n    # --------------------------------------------------\n\n    reconstruction_score = (\n        scene_recall\n        - purity_weight * (1.0 - cluster_purity)\n    )\n\n    return {\n        # Main metrics\n        \"scene_recall\": scene_recall,\n        \"mean_scene_recall\": mean_scene_recall,\n        \"cluster_purity\": cluster_purity,\n        \"mean_cluster_purity\": mean_cluster_purity,\n\n        # Combined proxy\n        \"reconstruction_score\": reconstruction_score,\n\n        # Diagnostics\n        \"num_scenes\": len(scene_details),\n        \"num_clusters\": len(cluster_details),\n        \"scene_details\": scene_details,\n        \"cluster_details\": cluster_details,\n    }\n\ndef run_pipeline_for_config( \n    dataset_name,\n    dataset=None,\n    nfeatures=1500,\n    min_matches=10,\n    matcher=\"flann\",\n    use_knn=True,\n    cnn_threshold=0.0,\n    use_cnn_filtering=False,\n):\n    dataset_path = os.path.join(train_path, dataset_name)\n\n    pipeline_kwargs={'nfeatures': nfeatures,\n        'min_matches': min_matches,\n        'matcher': matcher,\n        'use_knn': use_knn,\n        'train': True,\n        'use_custom_features': False,\n        'use_cnn_filtering': use_cnn_filtering,\n        \"workspace\": f\"colmap_pipeline_workspace_{dataset}\",\n        \"feature\": \"sift\",\n        }\n    \n    ds = pipeline = ImageMatchingDataset(\n        dataset=dataset_path,\n        # workspace_root=workspace_root,\n        **pipeline_kwargs,\n    )\n\n    ds.extract_features()\n    ds.pairwise_match()\n    ds.ransac_filter()\n    ds.prune_inliers()\n    ds.cluster_images()\n\n    metrics = clustering_score_proxy(ds)\n    metrics.update(\n        {\n            \"dataset\": dataset_name,\n            \"nfeatures\": nfeatures,\n            \"min_matches\": min_matches,\n            \"matcher\": matcher,\n            \"use_knn\": use_knn,\n            \"cnn_threshold\": cnn_threshold,\n            \"use_cnn_filtering\": use_cnn_filtering,\n            \"num_images\": len(ds.image_names),\n            \"num_clusters\": len(ds.clusters),\n        }\n    )\n    return metrics","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.323115Z","iopub.execute_input":"2026-06-28T15:00:49.323490Z","iopub.status.idle":"2026-06-28T15:00:49.352074Z","shell.execute_reply.started":"2026-06-28T15:00:49.323440Z","shell.execute_reply":"2026-06-28T15:00:49.351188Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# *Find the number of images for each train data-set*","metadata":{}},{"cell_type":"code","source":"train_datasets = os.listdir(train_path)\ntest_datasets = os.listdir(test_path)\n\n# Sort train datasets by number of images\nsorted_datasets = sorted(\n    [(name, len(os.listdir(os.path.join(test_path, name)))) for name in test_datasets],\n    key=lambda x: x[1]\n)\nprint(\"Train datasets sorted by number of images:\")\nfor name, count in sorted_datasets:\n    print(f\"{name}: {count} images\")\n\nsorted_datasets_train = sorted(\n    [(name, len(os.listdir(os.path.join(train_path, name)))) for name in train_datasets],\n    key=lambda x: x[1]\n)\nprint(\"Train datasets sorted by number of images:\")\nfor name, count in sorted_datasets_train:\n    print(f\"{name}: {count} images\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.353278Z","iopub.execute_input":"2026-06-28T15:00:49.353645Z","iopub.status.idle":"2026-06-28T15:00:49.391993Z","shell.execute_reply.started":"2026-06-28T15:00:49.353611Z","shell.execute_reply":"2026-06-28T15:00:49.390504Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# *Evaluation*","metadata":{}},{"cell_type":"code","source":"import os\nimport re\nimport numpy as np\nimport pandas as pd\nfrom collections import defaultdict\nfrom sklearn.metrics import normalized_mutual_info_score, adjusted_rand_score\n\ndef evaluate_train_dataset_full(\n    dataset_name,\n    pipeline_kwargs=None,\n    output_path=None,\n    train_path=train_path,\n    train_labels=train_labels,\n    train_thresholds=train_thresholds,\n    verbose=True,\n    plot = False,\n):\n    \"\"\"\n    Run the full COLMAP pipeline on one train dataset and evaluate:\n    - clustering against train_labels\n    - pose coverage and pose error on reconstructed images\n    - a proxy mAA using train_thresholds when available\n    \"\"\"\n    pipeline_kwargs = pipeline_kwargs or {}\n\n    dataset_path = os.path.join(train_path, dataset_name)\n    workspace_root = os.path.join(\"colmap_eval\", dataset_name)\n\n    pipeline = ImageMatchingDataset(\n        dataset=dataset_path,\n        # workspace_root=workspace_root,\n        **pipeline_kwargs,\n    )\n    \n\n    try:\n        combined_pose_df = pipeline.run_full_pipeline()\n        submission_df = pipeline.build_submission(output_path=output_path) if len(combined_pose_df) else pd.DataFrame()\n    except Exception as e:\n        if hasattr(pipeline, 'db'):\n            pipeline.db.close()\n        raise(e)\n    \n    \n    if plot:\n        pipeline.visualize_matches()\n    def parse_nums(value):\n        return np.array(\n            [float(x) for x in re.findall(r\"[-+]?\\d*\\.?\\d+(?:[eE][-+]?\\d+)?\", str(value))],\n            dtype=float,\n        )\n\n    def parse_rot(value):\n        arr = parse_nums(value)\n        # if arr.size == 1:\n        #     # Probably nan or invalid, return identity\n        #     return np.eye(3, dtype=float)\n        if arr.size != 9:\n            # raise ValueError(f\"Expected 9 values for rotation_matrix, got {arr.size}\")\n            return np.eye(3, dtype=float)\n        return arr.reshape(3, 3)\n\n    def parse_tra(value):\n        arr = parse_nums(value)\n        # if arr.size == 1:\n        #     # Probably nan or invalid, return zero vector\n        #     return np.zeros(3, dtype=float)\n        if arr.size != 3:\n            return np.zeros(3, dtype=float)\n            # raise ValueError(f\"Expected 3 values for translation_vector, got {arr.size}\")\n        return arr\n\n    def rot_error_deg(R_pred, R_gt):\n        R_rel = R_pred @ R_gt.T\n        cos_angle = np.clip((np.trace(R_rel) - 1.0) / 2.0, -1.0, 1.0)\n        return float(np.degrees(np.arccos(cos_angle)))\n\n    def camera_center(R_cw, t_cw):\n        return -R_cw.T @ t_cw\n\n    def umeyama_alignment(X, Y, with_scale=True):\n        \"\"\"\n        Align X to Y with a similarity transform.\n        \"\"\"\n        mu_x = X.mean(axis=0)\n        mu_y = Y.mean(axis=0)\n        Xc = X - mu_x\n        Yc = Y - mu_y\n\n        cov = (Yc.T @ Xc) / len(X)\n        U, S, Vt = np.linalg.svd(cov)\n        D = np.eye(3)\n        if np.linalg.det(U) * np.linalg.det(Vt) < 0:\n            D[-1, -1] = -1.0\n\n        R = U @ D @ Vt\n        scale = 1.0\n        if with_scale:\n            var_x = (Xc ** 2).sum() / len(X)\n            scale = float(np.trace(np.diag(S) @ D) / max(var_x, 1e-12))\n\n        t = mu_y - scale * (R @ mu_x)\n        return scale, R, t\n\n    def normalize_text(value):\n        return str(value).strip().lower()\n\n    def normalize_image(value):\n        return os.path.basename(str(value).replace(\"\\\\\", \"/\")).strip().lower()\n\n    def thresholds_for_scene(scene_name):\n        rows = train_thresholds[train_thresholds[\"dataset\"] == dataset_name]\n        if \"scene\" in rows.columns:\n            rows = rows[rows[\"scene\"] == scene_name]\n        if len(rows) == 0:\n            return np.array([0.01, 0.03, 0.1, 0.3, 1.0], dtype=float)\n        value = rows.iloc[0][\"thresholds\"]\n        thr = parse_nums(value)\n        if thr.size == 0:\n            return np.array([0.01, 0.03, 0.1, 0.3, 1.0], dtype=float)\n        return thr\n\n    gt_rows = train_labels[train_labels[\"dataset\"] == dataset_name].copy()\n    gt_rows = gt_rows[gt_rows[\"scene\"] != \"outliers\"].copy()\n    if len(gt_rows) == 0:\n        raise ValueError(f\"No labeled training rows found for dataset {dataset_name}\")\n\n    pred_rows = combined_pose_df.copy() if len(combined_pose_df) else pd.DataFrame(\n        columns=[\"dataset\", \"scene\", \"image\", \"rotation_matrix\", \"translation_vector\"]\n    )\n\n    for df in (gt_rows, pred_rows):\n        df[\"dataset_n\"] = df[\"dataset\"].map(normalize_text)\n        df[\"scene_n\"] = df[\"scene\"].map(normalize_text)\n        df[\"image_n\"] = df[\"image\"].map(normalize_image)\n\n    merged = gt_rows.merge(\n        pred_rows,\n        on=[\"dataset_n\", \"image_n\"],\n        how=\"inner\",\n        suffixes=(\"_gt\", \"_pred\"),\n    )\n\n    if len(merged) == 0:\n        merged = gt_rows.merge(\n            pred_rows,\n            on=[\"image_n\"],\n            how=\"inner\",\n            suffixes=(\"_gt\", \"_pred\"),\n        )\n\n    gt_scene_groups = defaultdict(set)\n    for _, row in gt_rows.iterrows():\n        gt_scene_groups[row[\"scene\"]].add(row[\"image\"])\n\n    pred_clusters = [set(cluster) for cluster in pipeline.clusters]\n\n    cluster_details = []\n    true_labels = []\n    pred_labels = []\n\n    for scene_name, scene_images in gt_scene_groups.items():\n        best = None\n        for idx, cluster in enumerate(pred_clusters):\n            overlap = len(scene_images & cluster)\n            if overlap == 0:\n                continue\n\n            precision = overlap / len(cluster)\n            recall = overlap / len(scene_images)\n            f1 = 0.0 if precision + recall == 0 else 2.0 * precision * recall / (precision + recall)\n            key = (overlap, f1, precision)\n\n            if best is None or key > best[0]:\n                best = (key, idx, cluster, overlap, precision, recall, f1)\n\n        if best is None:\n            cluster_details.append(\n                {\n                    \"scene\": scene_name,\n                    \"cluster_idx\": None,\n                    \"scene_size\": len(scene_images),\n                    \"cluster_size\": 0,\n                    \"overlap\": 0,\n                    \"precision\": 0.0,\n                    \"recall\": 0.0,\n                    \"f1\": 0.0,\n                }\n            )\n            continue\n\n        _, idx, cluster, overlap, precision, recall, f1 = best\n        cluster_details.append(\n            {\n                \"scene\": scene_name,\n                \"cluster_idx\": idx,\n                \"scene_size\": len(scene_images),\n                \"cluster_size\": len(cluster),\n                \"overlap\": overlap,\n                \"precision\": precision,\n                \"recall\": recall,\n                \"f1\": f1,\n            }\n        )\n\n        for img_name in scene_images:\n            true_labels.append(scene_name)\n            pred_labels.append(f\"cluster_{idx}\")\n\n    matched_clusters = [d for d in cluster_details if d[\"cluster_idx\"] is not None]\n    if verbose:\n        print(\"Cluster details:\")\n        for d in cluster_details:\n            print(\n                f\"Scene: {d['scene']}, Cluster idx: {d['cluster_idx']}, Scene size: {d['scene_size']}, \"\n                f\"Cluster size: {d['cluster_size']}, Overlap: {d['overlap']}, \"\n                f\"Precision: {d['precision']:.4f}, Recall: {d['recall']:.4f}, F1: {d['f1']:.4f}\"\n            )\n        \n    total_overlap = sum(d[\"overlap\"] for d in matched_clusters)\n    total_cluster_size = sum(d[\"cluster_size\"] for d in matched_clusters)\n    total_scene_size = sum(d[\"scene_size\"] for d in cluster_details)\n\n    cluster_precision = total_overlap / total_cluster_size if total_cluster_size else 0.0\n    cluster_recall = total_overlap / total_scene_size if total_scene_size else 0.0\n    cluster_f1 = 0.0 if cluster_precision + cluster_recall == 0 else (\n        2.0 * cluster_precision * cluster_recall / (cluster_precision + cluster_recall)\n    )\n\n    if len(true_labels) > 0:\n        nmi = normalized_mutual_info_score(true_labels, pred_labels)\n        ari = adjusted_rand_score(true_labels, pred_labels)\n    else:\n        nmi = 0.0\n        ari = 0.0\n\n    pose_details = []\n    pose_summary = {\n        \"coverage\": 0.0,\n        \"mean_rot_err_deg\": 0.0,\n        \"mean_center_err\": 0.0,\n        \"mAA_proxy\": 0.0,\n    }\n\n    if len(merged) > 0:\n        try:\n            merged = merged.copy()\n\n            R_gt = np.stack(merged[\"rotation_matrix_gt\"].map(parse_rot).to_list())\n            t_gt = np.stack(merged[\"translation_vector_gt\"].map(parse_tra).to_list())\n\n            R_pred_cw = np.stack(merged[\"rotation_matrix_pred\"].map(parse_rot).to_list())\n            t_pred_cw = np.stack(merged[\"translation_vector_pred\"].map(parse_tra).to_list())\n\n            R_pred_wc = np.transpose(R_pred_cw, (0, 2, 1))\n            t_pred_wc = np.array([-R_pred_wc[i] @ t_pred_cw[i] for i in range(len(t_pred_cw))])\n\n            rot_err_cw = np.array([rot_error_deg(R_pred_cw[i], R_gt[i]) for i in range(len(R_gt))])\n            rot_err_wc = np.array([rot_error_deg(R_pred_wc[i], R_gt[i]) for i in range(len(R_gt))])\n\n            use_wc = np.nanmean(rot_err_wc) < np.nanmean(rot_err_cw)\n            R_pred = R_pred_wc if use_wc else R_pred_cw\n            t_pred = t_pred_wc if use_wc else t_pred_cw\n            rot_err = rot_err_wc if use_wc else rot_err_cw\n\n            C_gt = np.stack([camera_center(R_gt[i], t_gt[i]) for i in range(len(R_gt))])\n            C_pred = np.stack([camera_center(R_pred[i], t_pred[i]) for i in range(len(R_pred))])\n\n            scale, R_align, t_align = umeyama_alignment(C_pred, C_gt, with_scale=True)\n            C_pred_aligned = (scale * (R_align @ C_pred.T)).T + t_align\n\n            center_err = np.linalg.norm(C_pred_aligned - C_gt, axis=1)\n\n            scene_names = merged[\"scene_gt\"].values if \"scene_gt\" in merged.columns else merged[\"scene\"].values\n            unique_scenes = sorted(set(scene_names))\n        except Exception as e:\n            display(merged)\n            print(\"Error during pose evaluation. Skipping pose metrics.\")\n            raise e\n        # for i in range(len(merged)):\n        #     print(f\"Image: {merged.iloc[i]['image_gt']}, Scene: {scene_names[i]}, Rot err (cw): {rot_err_cw[i]:.2f} deg, \"\n        #           f\"Rot err (wc): {rot_err_wc[i]:.2f} deg, Center err: {center_err[i]:.4f}\")\n        \n        # print(\"mean rot err cw:\", np.mean(rot_err_cw))\n        # print(\"mean rot err wc:\", np.mean(rot_err_wc))\n        # print(\"Umeyama scale:\", scale)\n\n        # print(\"mean pairwise GT baseline:\", np.mean(np.linalg.norm(C_gt[:, None] - C_gt[None, :], axis=-1)))\n        # print(\"mean pairwise predicted baseline:\", np.mean(np.linalg.norm(C_pred_aligned[:, None] - C_pred_aligned[None, :], axis=-1)))\n\n\n        # print(\"mean center err:\", np.mean(center_err))\n        # print(\"median center err:\", np.median(center_err))\n        \n        # print(\"GT R\")\n        # print(R_gt[0])\n\n        # print(\"Pred R\")\n        # print(R_pred[0])\n\n        # print(\"GT center\")\n        # print(C_gt[0])\n\n        # print(\"Pred center\")\n        # print(C_pred_aligned[0])\n\n        # print(\"C_pred range:\",\n        #     C_pred.min(axis=0),\n        #     C_pred.max(axis=0))\n\n        # print(\"C_gt range:\",\n        #     C_gt.min(axis=0),\n        #     C_gt.max(axis=0))\n\n        scene_mAA = []\n        for scene_name in unique_scenes:\n            idx = np.array([s == scene_name for s in scene_names], dtype=bool)\n            if idx.sum() == 0:\n                continue\n            thr = thresholds_for_scene(scene_name)\n            if len(thr) == 0:\n                continue\n            mAA_scene = float(np.mean([np.mean(center_err[idx] <= t) for t in thr]))\n            scene_mAA.append(mAA_scene)\n\n            pose_details.append(\n                {\n                    \"scene\": scene_name,\n                    \"num_registered\": int(idx.sum()),\n                    \"mean_rot_err_deg\": float(np.mean(rot_err[idx])),\n                    \"mean_center_err\": float(np.mean(center_err[idx])),\n                    \"mAA_proxy\": mAA_scene,\n                    \"thresholds\": thr.tolist(),\n                }\n            )\n\n        pose_summary = {\n            \"coverage\": float(len(merged) / len(gt_rows)) if len(gt_rows) else 0.0,\n            \"mean_rot_err_deg\": float(np.mean(rot_err)),\n            \"mean_center_err\": float(np.mean(center_err)),\n            \"mAA_proxy\": float(np.mean(scene_mAA)) if scene_mAA else 0.0,\n        }\n\n    result = {\n        \"dataset\": dataset_name,\n        \"num_images\": len(pipeline.image_names),\n        \"num_clusters\": len(pipeline.clusters),\n        \"num_scenes\": len(gt_scene_groups),\n        \"cluster_precision\": cluster_precision,\n        \"cluster_recall\": cluster_recall,\n        \"cluster_f1\": cluster_f1,\n        \"nmi\": nmi,\n        \"ari\": ari,\n        \"pose_coverage\": pose_summary[\"coverage\"],\n        \"mean_rot_err_deg\": pose_summary[\"mean_rot_err_deg\"],\n        \"mean_center_err\": pose_summary[\"mean_center_err\"],\n        \"mAA_proxy\": pose_summary[\"mAA_proxy\"],\n        \"cluster_details\": cluster_details,\n        \"pose_details\": pose_details,\n        \"pipeline\": pipeline,\n        \"submission\": submission_df,\n    }\n\n    if verbose:\n        print(f\"Dataset: {dataset_name}\")\n        print(f\"Images: {result['num_images']} | Clusters: {result['num_clusters']} | Scenes: {result['num_scenes']}\")\n        print(\n            f\"Cluster score proxy: precision={cluster_precision:.4f}, \"\n            f\"recall={cluster_recall:.4f}, f1={cluster_f1:.4f}\"\n        )\n        print(f\"NMI={nmi:.4f}, ARI={ari:.4f}\")\n        print(\n            f\"Pose coverage={pose_summary['coverage']:.4f}, \"\n            f\"mean rot err={pose_summary['mean_rot_err_deg']:.4f} deg, \"\n            f\"mean center err={pose_summary['mean_center_err']:.4f}, \"\n            f\"mAA proxy={pose_summary['mAA_proxy']:.4f}\"\n        )\n\n    return result","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.393361Z","iopub.execute_input":"2026-06-28T15:00:49.393812Z","iopub.status.idle":"2026-06-28T15:00:49.438910Z","shell.execute_reply.started":"2026-06-28T15:00:49.393761Z","shell.execute_reply":"2026-06-28T15:00:49.437782Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_config = {\n    'nfeatures': 1500,\n     'min_matches': 13,\n     'matcher': 'flann',\n     'use_knn': True,\n     'use_cnn': True,\n     'extractor': 'sift',\n     'use_custom': False,\n     'similarity_threshold': 0.5\n}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.440358Z","iopub.execute_input":"2026-06-28T15:00:49.440872Z","iopub.status.idle":"2026-06-28T15:00:49.464096Z","shell.execute_reply.started":"2026-06-28T15:00:49.440829Z","shell.execute_reply":"2026-06-28T15:00:49.462903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.465429Z","iopub.execute_input":"2026-06-28T15:00:49.465815Z","iopub.status.idle":"2026-06-28T15:00:49.483221Z","shell.execute_reply.started":"2026-06-28T15:00:49.465772Z","shell.execute_reply":"2026-06-28T15:00:49.482162Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Determine pipeline components","metadata":{}},{"cell_type":"code","source":"USE_RETRIEVAL_MATCHES = True   # ενεργοποίηση SuperPoint matches στο COLMAP\nUSE_ALIKE_MATCHES = True # ενεργοποιηση alike\nUSE_SIFTNN_MATCHES = True\nUSE_ENSEMBLE_RANSAC = True\nUSE_CROP_MATCHES   = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.484554Z","iopub.execute_input":"2026-06-28T15:00:49.484930Z","iopub.status.idle":"2026-06-28T15:00:49.502850Z","shell.execute_reply.started":"2026-06-28T15:00:49.484891Z","shell.execute_reply":"2026-06-28T15:00:49.501852Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\nif not submission :\n    evaluate_train_dataset_full(\n        'imc2023_haiper',\n        pipeline_kwargs={'nfeatures': best_config[\"nfeatures\"], 'min_matches': best_config[\"min_matches\"],\n            'matcher': best_config[\"matcher\"], 'use_knn': best_config[\"use_knn\"], 'train': True,\n            'use_custom_features': False, 'feature': 'sift', 'workspace': \"colmap_ens\",\n            'retrieval': 'SuperPoint', 'retrieval_args': {'min_matches': 100, 'resize_long': 1280}},\n        output_path=\"sub_ens.csv\", verbose=True, plot=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.891796Z","iopub.execute_input":"2026-06-28T15:00:49.892133Z","iopub.status.idle":"2026-06-28T15:00:49.897468Z","shell.execute_reply.started":"2026-06-28T15:00:49.892100Z","shell.execute_reply":"2026-06-28T15:00:49.896594Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if not submission :\n    evaluate_train_dataset_full(\n        'imc2023_haiper',\n        pipeline_kwargs={'nfeatures': best_config[\"nfeatures\"], 'min_matches': best_config[\"min_matches\"],\n            'matcher': best_config[\"matcher\"], 'use_knn': best_config[\"use_knn\"], 'train': True,\n            'use_custom_features': False, 'feature': 'sift', 'workspace': \"colmap_retr_matches\",\n            'retrieval': 'SuperPoint', 'retrieval_args': {'min_matches': 100, 'resize_long': 1280}},\n        output_path=\"sub_retr.csv\", verbose=True, plot=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.898877Z","iopub.execute_input":"2026-06-28T15:00:49.899202Z","iopub.status.idle":"2026-06-28T15:00:49.916920Z","shell.execute_reply.started":"2026-06-28T15:00:49.899175Z","shell.execute_reply":"2026-06-28T15:00:49.916011Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if not submission :\n    dataset = 'imc2023_haiper'\n    result = evaluate_train_dataset_full(\n        dataset,\n        pipeline_kwargs={'nfeatures': best_config[\"nfeatures\"],\n            'min_matches': best_config[\"min_matches\"],\n            'matcher': best_config[\"matcher\"],\n            'use_knn': best_config[\"use_knn\"],\n            'train': True,\n            'use_custom_features': False,\n            'use_cnn_filtering': True,\n            \"workspace\": f\"colmap_pipeline_workspace_{dataset}\",\n            \"feature\": \"sift\",\n            },\n        output_path=f\"submission_{dataset}.csv\",\n        verbose = True,\n        plot = True,\n    )\n    \n    display(pd.DataFrame(result[\"cluster_details\"]))\n    display(pd.DataFrame(result[\"pose_details\"]))\n    \n    # Quick retrieval check: SuperPoint vs CNN (no COLMAP, fast)\n    retr_test_dataset = 'imc2023_haiper'\n    \n    def quick_retrieval_eval(dataset_name, retrieval, retrieval_args=None):\n        ds = ImageMatchingDataset(\n            dataset=os.path.join(train_path, dataset_name),\n            nfeatures=best_config[\"nfeatures\"], min_matches=best_config[\"min_matches\"],\n            matcher=best_config[\"matcher\"], use_knn=best_config[\"use_knn\"], train=True,\n            use_custom_features=False, workspace=f\"colmap_quick_{dataset_name}_{retrieval}\",\n            feature=best_config[\"extractor\"], similarity_threshold=best_config[\"similarity_threshold\"],\n            retrieval=retrieval, retrieval_args=retrieval_args,   # <-- pick component here\n        )\n        ds.extract_features(); ds.pairwise_match(); ds.ransac_filter(); ds.prune_inliers(); ds.cluster_images()\n        m = clustering_score_proxy(ds)\n        return {\"retrieval\": retrieval, \"num_clusters\": len(ds.clusters),\n                \"scene_recall\": round(m[\"scene_recall\"], 4),\n                \"cluster_purity\": round(m[\"cluster_purity\"], 4),\n                \"reconstruction_score\": round(m[\"reconstruction_score\"], 4)}\n    \n    rows = [\n        quick_retrieval_eval(retr_test_dataset, \"CNN\"),\n        quick_retrieval_eval(retr_test_dataset, \"SuperPoint\", {\"min_matches\": 30, \"resize_long\": 1280}),\n    ]\n    display(pd.DataFrame(rows))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.918997Z","iopub.execute_input":"2026-06-28T15:00:49.919323Z","iopub.status.idle":"2026-06-28T15:00:49.935983Z","shell.execute_reply.started":"2026-06-28T15:00:49.919296Z","shell.execute_reply":"2026-06-28T15:00:49.934893Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# *Submission results construction*","metadata":{}},{"cell_type":"code","source":"# Execute full pipeline on all test datasets and save submissions\ntest_submissions = {}\n\nfor dataset_name, _ in sorted_datasets:\n    print(f\"\\n{'='*60}\")\n    print(f\"Processing test dataset: {dataset_name}\")\n    print(f\"{'='*60}\")\n    \n    try:\n        # Create pipeline with best config\n        dataset_path = os.path.join(test_path, dataset_name)\n        pipeline = ImageMatchingDataset(\n            dataset=dataset_path,\n            nfeatures=best_config[\"nfeatures\"],\n            min_matches=best_config[\"min_matches\"],\n            matcher=best_config[\"matcher\"],\n            use_knn=best_config[\"use_knn\"],\n            train=False,\n            use_custom_features = best_config['use_custom'],\n            workspace= f\"colmap_pipeline_workspace_{dataset_name}\",\n            feature = best_config['extractor'],\n            use_cnn_filtering = best_config['use_cnn'],\n            similarity_threshold = best_config['similarity_threshold'],\n            retrieval='SuperPoint',                                      # new\n            retrieval_args={'min_matches': 100, 'resize_long': 1280},    # new\n        )\n        \n        # Run full pipeline (feature extraction, matching, clustering, COLMAP)\n        print(f\"Running full pipeline...\")\n        combined_pose_df = pipeline.run_full_pipeline()\n        \n        # Save submission\n        output_path = f\"submission_{dataset_name}.csv\"\n        print(f\"Saving submission to {output_path}...\")\n        submission_df = pipeline.build_submission(output_path=output_path)\n        \n        test_submissions[dataset_name] = {\n            \"num_images\": len(pipeline.image_names),\n            \"num_clusters\": len(pipeline.clusters),\n            \"num_registered\": len(combined_pose_df),\n            \"submission_path\": output_path,\n        }\n        \n        print(f\"✓ Completed {dataset_name}: {len(combined_pose_df)} images registered\")\n        \n    except Exception as e:\n        print(f\"✗ Error processing {dataset_name}: {repr(e)}\")\n        test_submissions[dataset_name] = {\"error\": str(e)}\n\n# Summary\nprint(f\"\\n{'='*60}\")\nprint(\"Test Dataset Processing Summary\")\nprint(f\"{'='*60}\")\nfor dataset_name, info in test_submissions.items():\n    if \"error\" in info:\n        print(f\"{dataset_name}: ERROR - {info['error']}\")\n    else:\n        print(f\"{dataset_name}: {info['num_registered']} registered → {info['submission_path']}\")","metadata":{"trusted":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2026-06-28T15:00:49.937195Z","iopub.execute_input":"2026-06-28T15:00:49.937675Z","iopub.status.idle":"2026-06-28T15:34:30.209357Z","shell.execute_reply.started":"2026-06-28T15:00:49.937631Z","shell.execute_reply":"2026-06-28T15:34:30.208303Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"gt = train_labels[train_labels.dataset == 'imc2023_haiper']\nprint(gt.scene.value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:34:30.210921Z","iopub.execute_input":"2026-06-28T15:34:30.211963Z","iopub.status.idle":"2026-06-28T15:34:30.221571Z","shell.execute_reply.started":"2026-06-28T15:34:30.211914Z","shell.execute_reply":"2026-06-28T15:34:30.220651Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if True:\n    # Merge submissions into final submission.csv\n    final_submission_rows = []\n    for dataset_name, info in test_submissions.items():\n        if \"error\" in info:\n            print(f\"Skipping {dataset_name} due to error: {info['error']}\")\n            continue\n        \n        submission_path = info[\"submission_path\"]\n        if not os.path.exists(submission_path):\n            print(f\"Submission file not found for {dataset_name}: {submission_path}\")\n            continue\n        \n        df = pd.read_csv(submission_path)\n        final_submission_rows.append(df)\n    if final_submission_rows:\n        final_submission_df = pd.concat(final_submission_rows, ignore_index=True)\n        identity_R = \"1;0;0;0;1;0;0;0;1\"\n        zero_t = \"0;0;0\"\n        \n        sample_submission = pd.read_csv(\n            \"/kaggle/input/competitions/image-matching-challenge-2025/sample_submission.csv\"\n        )\n        \n        predictions = final_submission_df.copy()\n        \n        # Remove duplicates\n        predictions = predictions.drop_duplicates(\n            subset=[\"dataset\", \"image\"],\n            keep=\"first\"\n        )\n        \n        merged = sample_submission.merge(\n            predictions,\n            on=[\"dataset\", \"image\"],\n            how=\"left\",\n            suffixes=(\"_sub\", \"_pred\"),\n            validate=\"one_to_one\"\n        )\n        display(merged.head())\n        final = pd.DataFrame()\n\n        final[\"image_id\"] = merged[\"image_id_sub\"]\n        final[\"dataset\"] = merged[\"dataset\"]\n        final[\"scene\"] = merged[\"scene_pred\"].fillna(\"outliers\")\n        final[\"image\"] = merged[\"image\"]\n        \n        \n        final[\"rotation_matrix\"] = merged[\"rotation_matrix_pred\"].fillna(identity_R)\n        final[\"translation_vector\"] = merged[\"translation_vector_pred\"].fillna(zero_t)\n        final.to_csv(\"submission.csv\", index=False)\n        display(final.head())\n        print(f\"\\nFinal submission saved to final_submission.csv with {len(final)} rows\")\n        print(os.listdir())\n    else:\n        sample_submission = pd.read_csv('/kaggle/input/competitions/image-matching-challenge-2025/sample_submission.csv')\n        sample_submission[\"rotation_matrix\"] = \"1;0;0;0;1;0;0;0;1\"  # Identity matrix as a placeholder\n        sample_submission[\"translation_vector\"] = \"0;0;0\"  # Zero translation\n        sample_submission.to_csv(\"submission.csv\", index=False)\n        \nelse:\n    sample_submission = pd.read_csv('/kaggle/input/competitions/image-matching-challenge-2025/sample_submission.csv')\n    #sample_submission.head()\n    #checking column named to have in dummy submission\n    \n    # Create dummy values\n    sample_submission[\"rotation_matrix\"] = \"1;0;0;0;1;0;0;0;1\"  # Identity matrix as a placeholder\n    sample_submission[\"translation_vector\"] = \"0;0;0\"  # Zero translation\n    \n    # Save the dummy submission file\n    sample_submission.to_csv(\"submission.csv\", index=False)\n    \n    print(\"Dummy submission file created successfully!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:34:30.223026Z","iopub.execute_input":"2026-06-28T15:34:30.223502Z","iopub.status.idle":"2026-06-28T15:34:30.335609Z","shell.execute_reply.started":"2026-06-28T15:34:30.223459Z","shell.execute_reply":"2026-06-28T15:34:30.334849Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submitted = pd.read_csv(\"submission.csv\")\ndisplay(submitted.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-28T15:34:30.336999Z","iopub.execute_input":"2026-06-28T15:34:30.337374Z","iopub.status.idle":"2026-06-28T15:34:30.359125Z","shell.execute_reply.started":"2026-06-28T15:34:30.337332Z","shell.execute_reply":"2026-06-28T15:34:30.358198Z"}},"outputs":[],"execution_count":null}]}