{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":91498,"databundleVersionId":11655853,"isSourceIdPinned":false},{"sourceType":"datasetVersion","sourceId":7884485,"datasetId":4628051,"databundleVersionId":7990559},{"sourceType":"datasetVersion","sourceId":11924468,"datasetId":6988459,"databundleVersionId":12432458},{"sourceType":"modelInstanceVersion","sourceId":17555,"databundleVersionId":7980546,"modelInstanceId":14611,"modelId":22086,"isSourceIdPinned":false},{"sourceType":"modelInstanceVersion","sourceId":4534,"databundleVersionId":6346558,"modelInstanceId":3326,"modelId":986,"isSourceIdPinned":false},{"sourceType":"modelInstanceVersion","sourceId":17191,"databundleVersionId":7971915,"modelInstanceId":14317,"modelId":21716,"isSourceIdPinned":false},{"sourceType":"kernelVersion","sourceId":301218867,"isSourceIdPinned":false}],"dockerImageVersionId":30919,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install --no-index --find-links=/kaggle/input/cyprus-3d-reconstruction-mast3r-wo-biplet/mast3r-wheels -r /kaggle/input/notebooks/stpeteishii/cyprus-3d-reconstruction-mast3r-wo-biplet/mast3r/requirements.txt\n!pip install --no-index /kaggle/input/imc2024-packages-lightglue-rerun-kornia/* --no-deps\n!mkdir -p /root/.cache/torch/hub/checkpoints\n!cp /kaggle/input/aliked/pytorch/aliked-n16/1/aliked-n16.pth /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth /root/.cache/torch/hub/checkpoints/\n!cp /kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth /root/.cache/torch/hub/checkpoints/aliked_lightglue_v0-1_arxiv-pth\n\nimport sys\nimport os\n\nINPUT_ROOT = '/kaggle/input/notebooks/stpeteishii/cyprus-3d-reconstruction-mast3r-wo-biplet'\nmast3r_source = f'{INPUT_ROOT}/mast3r'\n\nMAST3R_CKPT_PATH = f'{INPUT_ROOT}/mast3r/checkpoints/MASt3R_ViTLarge_BaseDecoder_512_catmlpdpt_metric.pth'\n\nif mast3r_source not in sys.path:\n    sys.path.append(mast3r_source)\n    dust3r_path = os.path.join(mast3r_source, 'dust3r')\n    if os.path.exists(dust3r_path) and dust3r_path not in sys.path:\n        sys.path.append(dust3r_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:05.385203Z","iopub.execute_input":"2026-04-14T12:08:05.385616Z","iopub.status.idle":"2026-04-14T12:08:10.954290Z","shell.execute_reply.started":"2026-04-14T12:08:05.385584Z","shell.execute_reply":"2026-04-14T12:08:10.953290Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings(\"ignore\", category=FutureWarning)\nfrom tqdm import tqdm\nfrom time import time, sleep\nimport gc\nimport numpy as np\nimport h5py\nimport dataclasses\nimport pandas as pd\nfrom IPython.display import clear_output\nfrom collections import defaultdict\nfrom copy import deepcopy\nfrom PIL import Image\n\nimport cv2\nimport torch\nimport torch.nn.functional as F\nimport torchvision.transforms as transforms\nimport kornia as K\nimport kornia.feature as KF\n\nimport torch\nfrom lightglue import match_pair\nfrom lightglue import ALIKED, LightGlue\nfrom lightglue.utils import load_image, rbd\nfrom transformers import AutoImageProcessor, AutoModel\n\ntry:\n    from mast3r.model import AsymmetricMASt3R\n    \n    try:\n        import mast3r.utils.path_to_dust3r as path_to_dust3r\n    except ImportError:\n        print(\"未找到 path_to_dust3r\")\n\nexcept ImportError as e:\n    print(f\"MASt3R 导入失败: {e}\")\n\nUTILS_PATH = '/kaggle/input/imc25-utils'\nif os.path.exists(UTILS_PATH):\n    if UTILS_PATH not in sys.path:\n        sys.path.append(UTILS_PATH)\n    try:\n        from database import *\n        from h5_to_db import *\n        import metric\n    except ImportError as e:\n        print(f\"工具子模块加载失败: {e}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:10.955898Z","iopub.execute_input":"2026-04-14T12:08:10.956132Z","iopub.status.idle":"2026-04-14T12:08:10.965607Z","shell.execute_reply.started":"2026-04-14T12:08:10.956112Z","shell.execute_reply":"2026-04-14T12:08:10.964795Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import PIL.Image\nimport PIL.ImageOps\nimport cv2\nimport numpy as np\n\n# 加入防重复运行的锁\nif not hasattr(PIL.Image, '_is_patched_for_exif'):\n    # 备份原始函数\n    _original_pil_open = PIL.Image.open\n    _original_cv2_imread = cv2.imread\n\n    def _patched_pil_open(*args, **kwargs):\n        img = _original_pil_open(*args, **kwargs)\n        try:\n            img = PIL.ImageOps.exif_transpose(img)\n        except:\n            pass\n        return img\n\n    def _patched_cv2_imread(*args, **kwargs):\n        try:\n            pil_img = _patched_pil_open(args[0])\n            img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)\n        except:\n            img = _original_cv2_imread(*args, **kwargs)\n        return img\n\n    PIL.Image.open = _patched_pil_open\n    cv2.imread = _patched_cv2_imread\n    PIL.Image._is_patched_for_exif = True","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:10.967574Z","iopub.execute_input":"2026-04-14T12:08:10.967822Z","iopub.status.idle":"2026-04-14T12:08:11.279529Z","shell.execute_reply.started":"2026-04-14T12:08:10.967803Z","shell.execute_reply":"2026-04-14T12:08:11.278677Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(device)\n\ntry:\n    mast3r_model = AsymmetricMASt3R.from_pretrained(MAST3R_CKPT_PATH).to(device)\n    mast3r_model.eval()\n    \n    params = sum(p.numel() for p in mast3r_model.parameters()) / 1e6\nexcept Exception as e:\n    print(f\"模型加载出错: {e}\")\n\ntorch.cuda.empty_cache()\ngc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:11.281006Z","iopub.execute_input":"2026-04-14T12:08:11.281358Z","iopub.status.idle":"2026-04-14T12:08:21.857199Z","shell.execute_reply.started":"2026-04-14T12:08:11.281323Z","shell.execute_reply":"2026-04-14T12:08:21.856237Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.hub\n\nALIKED_MODEL_PATH = '/kaggle/input/aliked/pytorch/aliked-n16/1/aliked-n16.pth' \nLIGHTGLUE_MODEL_PATH = '/kaggle/input/lightglue/pytorch/aliked/1/aliked_lightglue.pth'\n\nif not hasattr(torch.hub, '_is_patched_for_offline'):\n    _original_load_url = torch.hub.load_state_dict_from_url\n\n    def offline_load_state_dict_from_url(url, *args, **kwargs):\n        if 'aliked-n16' in url:\n            return torch.load(ALIKED_MODEL_PATH, map_location='cpu')\n        if 'aliked_lightglue' in url:\n            return torch.load(LIGHTGLUE_MODEL_PATH, map_location='cpu')\n        raise ValueError(f\"拦截到未知的网络请求: {url}\")\n\n    torch.hub.load_state_dict_from_url = offline_load_state_dict_from_url\n    torch.hub._is_patched_for_offline = True\n\nfrom lightglue import ALIKED, LightGlue\nfrom lightglue.utils import load_image\n\naliked_extractor = ALIKED(max_num_keypoints=4096, detection_threshold=0.01).eval().to(device)\nlg_matcher = LightGlue(features='aliked').eval().to(device)\nprint(\"ALIKED + LightGlue 加载完成\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:21.858091Z","iopub.execute_input":"2026-04-14T12:08:21.858421Z","iopub.status.idle":"2026-04-14T12:08:22.066513Z","shell.execute_reply.started":"2026-04-14T12:08:21.858389Z","shell.execute_reply":"2026-04-14T12:08:22.065780Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport torch\nimport kornia as K\nimport numpy as np\nimport h5py\nfrom tqdm import tqdm\nfrom PIL import Image\nimport torchvision.transforms as transforms\nimport torch.nn.functional as F\nfrom transformers import AutoImageProcessor, AutoModel\n\nfrom dust3r.inference import inference\nfrom dust3r.utils.image import load_images\nfrom mast3r.fast_nn import fast_reciprocal_NNs\n\n# 基础图像加载\ndef load_torch_image(fname, device=torch.device('cpu')):\n    img = K.io.load_image(fname, K.io.ImageLoadType.RGB32, device=device)[None, ...]\n    return img\n\n# 全局特征提取（DINOv2）\ndef get_global_desc(fnames, device=torch.device('cpu')):\n    model_path = '/kaggle/input/dinov2/pytorch/base/1'\n    processor = AutoImageProcessor.from_pretrained(model_path)\n    model = AutoModel.from_pretrained(model_path).eval().to(device)\n   \n    global_descs = []\n    for img_fname in tqdm(fnames, desc=\"Global Descriptors\"):\n        img = Image.open(img_fname).convert('RGB')\n        inputs = processor(images=img, return_tensors=\"pt\").to(device)\n        with torch.inference_mode():\n            outputs = model(**inputs)\n            desc = F.normalize(outputs.last_hidden_state[:, 0], dim=1)\n        global_descs.append(desc.cpu())\n    return torch.cat(global_descs, dim=0)\n\n# 图像对筛选\ndef get_image_pairs_shortlist(fnames, sim_th=0.3, min_pairs=20, exhaustive_if_less=15, device=torch.device('cpu')):\n    num_imgs = len(fnames)\n    if num_imgs <= exhaustive_if_less:\n        return [(i, j) for i in range(num_imgs) for j in range(i+1, num_imgs)]\n   \n    descs = get_global_desc(fnames, device=device)\n    dm = torch.cdist(descs, descs, p=2).numpy()\n    dist_th = (2 * (1 - sim_th)) ** 0.5\n   \n    matching_list = []\n    for i in range(num_imgs):\n        idxs = np.argsort(dm[i])[1:min_pairs + 10]\n        for idx in idxs:\n            if dm[i][idx] <= dist_th:\n                matching_list.append(tuple(sorted((i, int(idx)))))\n   \n    return sorted(list(set(matching_list)))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:22.067266Z","iopub.execute_input":"2026-04-14T12:08:22.067559Z","iopub.status.idle":"2026-04-14T12:08:22.077154Z","shell.execute_reply.started":"2026-04-14T12:08:22.067536Z","shell.execute_reply":"2026-04-14T12:08:22.076304Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import contextlib\nimport sys\nimport gc\nimport os\nfrom tqdm import tqdm\nimport h5py\nimport torch\nimport numpy as np\n\nfrom dust3r.utils.image import load_images as original_load_images\nfrom dust3r.inference import inference\nfrom mast3r.fast_nn import fast_reciprocal_NNs\nfrom lightglue.utils import load_image\n\ndef filter_pairs_with_mast3r(img_fnames, index_pairs, model, device='cuda', min_matches=15):\n    verified_pairs = []\n    \n    def silent_load_images(paths):\n        with contextlib.redirect_stdout(sys.stderr):\n            return original_load_images(paths, size=512, verbose=False)\n\n    for idx1, idx2 in tqdm(index_pairs, desc=\"MASt3R Pair Verification\", dynamic_ncols=True):\n        path1, path2 = img_fnames[idx1], img_fnames[idx2]\n        images = silent_load_images([path1, path2])\n        \n        with torch.inference_mode():\n            try:\n                output = inference([tuple(images)], model, device, batch_size=1, verbose=False)\n                # 解析 MASt3R 输出\n                if isinstance(output, dict):\n                    pred1, pred2 = output['pred1'], output['pred2']\n                else:\n                    item = output[0] if isinstance(output, (list, tuple)) else output\n                    pred1 = item.get('pred1') if isinstance(item, dict) else item[1]\n                    pred2 = item.get('pred2') if isinstance(item, dict) else item[3]\n                \n                # 获取匹配点数量\n                matches_im0, _ = fast_reciprocal_NNs(\n                    pred1['desc'].squeeze(0), pred2['desc'].squeeze(0),\n                    subsample_or_initxy1=8, device=device\n                )\n                \n                # 如果 MASt3R 认为这对图像有足够的共视区域，则保留\n                if matches_im0 is not None and len(matches_im0) >= min_matches:\n                    verified_pairs.append((idx1, idx2))\n                    \n                del output, pred1, pred2, matches_im0\n                torch.cuda.empty_cache()\n                \n            except Exception as e:\n                torch.cuda.empty_cache()\n                continue\n                \n    return verified_pairs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:22.078061Z","iopub.execute_input":"2026-04-14T12:08:22.078353Z","iopub.status.idle":"2026-04-14T12:08:22.091919Z","shell.execute_reply.started":"2026-04-14T12:08:22.078321Z","shell.execute_reply":"2026-04-14T12:08:22.091099Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def match_with_aliked_lightglue(img_fnames, verified_pairs, extractor, matcher, feature_dir='.featureout', device='cuda'):\n    if not os.path.isdir(feature_dir):\n        os.makedirs(feature_dir)\n        \n    match_path = f'{feature_dir}/matches.h5'\n    kp_path = f'{feature_dir}/keypoints.h5'\n    \n    extracted_features = {}\n    \n    with h5py.File(kp_path, mode='w') as f_kp, h5py.File(match_path, mode='w') as f_match:\n        for idx1, idx2 in tqdm(verified_pairs, desc=\"ALIKED+LightGlue Matching\", dynamic_ncols=True):\n            path1, path2 = img_fnames[idx1], img_fnames[idx2]\n            key1, key2 = os.path.basename(path1), os.path.basename(path2)\n            \n            with torch.inference_mode():\n                # 提取特征\n                if key1 not in extracted_features:\n                    img1_tensor = load_image(path1).to(device)\n                    extracted_features[key1] = extractor.extract(img1_tensor)\n                \n                if key2 not in extracted_features:\n                    img2_tensor = load_image(path2).to(device)\n                    extracted_features[key2] = extractor.extract(img2_tensor)\n                \n                feats0, feats1 = extracted_features[key1], extracted_features[key2]\n                \n                # 特征匹配\n                matches01 = matcher({\"image0\": feats0, \"image1\": feats1})\n                \n                matches = matches01['matches'][0].cpu().numpy()\n                kpts0 = feats0['keypoints'][0].cpu().numpy()\n                kpts1 = feats1['keypoints'][0].cpu().numpy()\n                \n            # 写入关键点\n            if key1 not in f_kp:\n                f_kp.create_dataset(key1, data=kpts0)\n            if key2 not in f_kp:\n                f_kp.create_dataset(key2, data=kpts1)\n                \n            # 写入匹配关系\n            if len(matches) > 0:\n                k1, k2 = key1, key2\n                m = matches\n                if k1 > k2:\n                    k1, k2 = k2, k1\n                    m = m[:, [1, 0]]\n                \n                # 嵌套的 Group 结构\n                group = f_match.require_group(k1)\n                # 防止由于前面的遍历导致同一对图片被重复写入报错\n                if k2 not in group:\n                    group.create_dataset(k2, data=m)\n\n    del extracted_features\n    torch.cuda.empty_cache()\n    gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:22.094204Z","iopub.execute_input":"2026-04-14T12:08:22.094491Z","iopub.status.idle":"2026-04-14T12:08:22.106203Z","shell.execute_reply.started":"2026-04-14T12:08:22.094459Z","shell.execute_reply":"2026-04-14T12:08:22.105338Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nfrom pathlib import Path\n\n@dataclasses.dataclass\nclass Prediction:\n    image_id: str | None\n    dataset: str\n    scene: str\n    filename: str\n\nis_train = True \ndata_dir = '/kaggle/input/image-matching-challenge-2025'\nworkdir = '/kaggle/working/result/'\n\nwork_path = Path(workdir)\nif work_path.exists():\n    shutil.rmtree(work_path)\nwork_path.mkdir(parents=True, exist_ok=True)\n\nif is_train:\n    csv_path = os.path.join(data_dir, 'train_labels.csv')\nelse:\n    csv_path = os.path.join(data_dir, 'sample_submission.csv')\n\nif not os.path.exists(csv_path):\n    print(f\"未找到 CSV 文件 {csv_path}\")\nelse:\n    competition_data = pd.read_csv(csv_path)\n\n    samples = defaultdict(list)\n\n    for _, row in competition_data.iterrows():\n        dataset = getattr(row, 'dataset', 'default_ds')\n        scene = getattr(row, 'scene', 'default_scene')\n        image_name = getattr(row, 'image', row.get('image_id', 'unknown.jpg'))\n        \n        image_id = None if is_train else getattr(row, 'image_id', None)\n\n        samples[dataset].append(\n            Prediction(\n                image_id=image_id,\n                dataset=dataset,\n                scene=scene,\n                filename=image_name\n            )\n        )\n\n    for ds, preds in samples.items():\n        print(f\"Dataset: {ds:<15} -> 包含图片总数: {len(preds)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T12:08:22.107305Z","iopub.execute_input":"2026-04-14T12:08:22.107615Z","iopub.status.idle":"2026-04-14T12:08:22.270117Z","shell.execute_reply.started":"2026-04-14T12:08:22.107580Z","shell.execute_reply":"2026-04-14T12:08:22.269413Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport gc\nimport time\nfrom copy import deepcopy\nimport pycolmap\n\ngc.collect()\ntorch.cuda.empty_cache()\n\ndatasets_to_process = ['amy_gardens', 'ETs', 'fbk_vineyard', 'stairs'] if is_train else None\n\nfor dataset, predictions in samples.items():\n    if datasets_to_process and dataset not in datasets_to_process:\n        continue\n\n    images_dir = os.path.join(data_dir, 'train' if is_train else 'test', dataset)\n    \n    images_paths = []\n    valid_predictions = []\n    for p in predictions:\n        full_path = os.path.join(images_dir, p.filename)\n        if os.path.exists(full_path):\n            images_paths.append(full_path)\n            valid_predictions.append(p)\n\n    if len(images_paths) < 2:\n        print(f\"跳过 {dataset} (图片数量不足)\")\n        continue\n\n    print(f\"开始全局处理 Dataset: {dataset} | 共 {len(images_paths)} 张图片\")\n\n    filename_to_index = {p.filename: idx for idx, p in enumerate(valid_predictions)}\n\n    feature_dir = os.path.join(workdir, 'featureout', f\"{dataset}\")\n    os.makedirs(feature_dir, exist_ok=True)\n    database_path = os.path.join(feature_dir, 'colmap.db')\n\n    try:\n        # DINOv2 粗略提名\n        index_pairs = get_image_pairs_shortlist(\n            images_paths, sim_th=0.3, min_pairs=30, exhaustive_if_less=20, device=device\n        )\n        \n        # MASt3R 精细验证\n        verified_pairs = filter_pairs_with_mast3r(\n            images_paths, index_pairs, model=mast3r_model, device=device, min_matches=15\n        )\n        \n        # ALIKED + LightGlue 高精度局部匹配\n        match_with_aliked_lightglue(\n            images_paths, verified_pairs, extractor=aliked_extractor, matcher=lg_matcher, feature_dir=feature_dir, device=device\n        )\n\n        # COLMAP 导入\n        if os.path.isfile(database_path):\n            os.remove(database_path)\n\n        import_into_colmap(images_dir, feature_dir=feature_dir, database_path=database_path)\n\n        # 几何验证\n        pairs_path = os.path.join(feature_dir, 'pairs.txt')\n        with open(pairs_path, 'w') as f:\n            for idx1, idx2 in index_pairs:\n                name1 = os.path.basename(images_paths[idx1])\n                name2 = os.path.basename(images_paths[idx2])\n                f.write(f\"{name1} {name2}\\n\")\n\n        # 针对 4 大数据集的动态 COLMAP 参数分配器\n        v_opts = pycolmap.TwoViewGeometryOptions()\n        m_opts = pycolmap.IncrementalPipelineOptions()\n        \n        # ETs & stairs\n        if dataset in ['ETs', 'stairs']:\n            v_opts.min_num_inliers = 15\n            ransac_error = 2.0  \n            reproj_error = 4.0  \n            \n        # amy_gardens & fbk_vineyard\n        elif dataset in ['amy_gardens', 'fbk_vineyard']:\n            v_opts.min_num_inliers = 20  \n            ransac_error = 8.0  \n            reproj_error = 8.0\n            \n        else:\n            v_opts.min_num_inliers = 15\n            ransac_error = 4.0\n            reproj_error = 6.0\n\n        # 应用几何验证阈值\n        if hasattr(v_opts, 'ransac'):\n            v_opts.ransac.max_error = ransac_error\n        else:\n            v_opts.max_error = ransac_error\n            \n        pycolmap.verify_matches(database_path, pairs_path, v_opts)\n\n        # 增量重建输出路径\n        output_path = os.path.join(feature_dir, 'colmap_rec')\n        os.makedirs(output_path, exist_ok=True)\n\n        # 应用增量重建阈值\n        m_opts.min_model_size = 3\n        m_opts.max_num_models = 3\n        \n        if hasattr(m_opts, 'mapper'):\n            m_opts.mapper.init_min_num_inliers = 20\n            m_opts.mapper.abs_pose_min_num_inliers = 15\n            m_opts.mapper.filter_max_reproj_error = reproj_error \n        else:\n            m_opts.init_min_num_inliers = 20\n            m_opts.abs_pose_min_num_inliers = 15\n            m_opts.filter_max_reproj_error = reproj_error\n\n        maps = pycolmap.incremental_mapping(\n            database_path=database_path,\n            image_path=images_dir,\n            output_path=output_path,\n            options=m_opts\n        )\n       \n        # 提取位姿\n        registered_count = 0\n        if maps and len(maps) > 0:\n            best_map = maps[0]\n            for colmap_img in best_map.images.values():\n                if colmap_img.name in filename_to_index:\n                    p_idx = filename_to_index[colmap_img.name]\n                    valid_predictions[p_idx].rotation = deepcopy(colmap_img.cam_from_world.rotation.matrix())\n                    valid_predictions[p_idx].translation = deepcopy(colmap_img.cam_from_world.translation)\n                    registered_count += 1\n            print(f\"成功注册: {registered_count}/{len(images_paths)} 张图片\")\n        else:\n            print(\"未生成有效重建模型\")\n\n    except Exception as e:\n        print(f\"场景 {scene} 执行失败: {e}\")\n\n    gc.collect()\n    torch.cuda.empty_cache()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T14:16:10.953403Z","iopub.execute_input":"2026-04-14T14:16:10.953800Z","iopub.status.idle":"2026-04-14T18:06:40.517755Z","shell.execute_reply.started":"2026-04-14T14:16:10.953775Z","shell.execute_reply":"2026-04-14T18:06:40.516873Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"array_to_str = lambda arr: ';'.join([f\"{x:.09f}\" for x in arr.flatten()]) if arr is not None else None\nnone_to_str = lambda n: ';'.join(['nan'] * n)\n\nsubmission_file = '/kaggle/working/submission.csv'\n\nwith open(submission_file, 'w') as f:\n    if is_train:\n        f.write('dataset,scene,image,rotation_matrix,translation_vector\\n')\n        \n        for ds, preds in samples.items():\n            for p in preds:\n                if not hasattr(p, 'rotation'): p.rotation = None\n                if not hasattr(p, 'translation'): p.translation = None\n                \n                rot_str = none_to_str(9) if p.rotation is None else array_to_str(p.rotation)\n                trans_str = none_to_str(3) if p.translation is None else array_to_str(p.translation)\n                f.write(f\"{ds},{p.scene},{p.filename},{rot_str},{trans_str}\\n\")\n    else:\n        f.write('image_id,dataset,scene,image,rotation_matrix,translation_vector\\n')\n        \n        for ds, preds in samples.items():\n            for p in preds:\n                if not hasattr(p, 'rotation'): p.rotation = None\n                if not hasattr(p, 'translation'): p.translation = None\n                \n                rot_str = none_to_str(9) if p.rotation is None else array_to_str(p.rotation)\n                trans_str = none_to_str(3) if p.translation is None else array_to_str(p.translation)\n                \n                final_scene = getattr(p, 'scene', f\"unregistered_{p.filename}\") \n                \n                f.write(f\"{p.image_id},{ds},{final_scene},{p.filename},{rot_str},{trans_str}\\n\")\n\nprint(f\"文件路径: {submission_file}\")\n!head -10 {submission_file}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T18:08:16.595017Z","iopub.execute_input":"2026-04-14T18:08:16.595323Z","iopub.status.idle":"2026-04-14T18:08:16.866201Z","shell.execute_reply.started":"2026-04-14T18:08:16.595302Z","shell.execute_reply":"2026-04-14T18:08:16.864715Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Definitely Compute results if running on the training set.\n# Do not do this when submitting a notebook for scoring. All you have to do is save your submission to /kaggle/working/submission.csv.\n\nif is_train:\n    t = time.time()\n    final_score, dataset_scores = metric.score(\n        gt_csv='/kaggle/input/image-matching-challenge-2025/train_labels.csv',\n        user_csv=submission_file,\n        thresholds_csv='/kaggle/input/image-matching-challenge-2025/train_thresholds.csv',\n        mask_csv=None if is_train else os.path.join(data_dir, 'mask.csv'),\n        inl_cf=0,\n        strict_cf=-1,\n        verbose=True,\n    )\n    print(f'Computed metric in: {time.time() - t:.02f} sec.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-14T18:08:20.776544Z","iopub.execute_input":"2026-04-14T18:08:20.776852Z","iopub.status.idle":"2026-04-14T18:09:11.972849Z","shell.execute_reply.started":"2026-04-14T18:08:20.776829Z","shell.execute_reply":"2026-04-14T18:09:11.972031Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}