{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":12042529,"sourceType":"datasetVersion","datasetId":7577927}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Setup","metadata":{"_uuid":"bfb77323-0550-4798-91e1-213389357d37","_cell_guid":"8deeee4e-164c-4dcc-a110-c48a6c87427e","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"!pip install faiss-cpu ftfy\n!pip install git+https://github.com/openai/CLIP.git","metadata":{"_uuid":"4974dab8-b8e7-43f5-87ba-8d7716ffd61a","_cell_guid":"0bfa735f-024c-4ed3-9964-0c8759035ed9","trusted":true,"execution":{"iopub.status.busy":"2025-06-05T16:19:21.156221Z","iopub.execute_input":"2025-06-05T16:19:21.156480Z","iopub.status.idle":"2025-06-05T16:20:51.865382Z","shell.execute_reply.started":"2025-06-05T16:19:21.156459Z","shell.execute_reply":"2025-06-05T16:20:51.864458Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport shutil\nfrom pathlib import Path\nfrom typing import Callable, List, Optional, Union\n\nimport faiss\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm.auto import tqdm\n\nimport torch\nfrom torch.utils.data import DataLoader, Dataset\nfrom torchvision import datasets, transforms\nfrom torchvision.models.feature_extraction import create_feature_extractor\nimport torch.nn.functional as F","metadata":{"_uuid":"d8dd186a-c32a-4cef-81d3-a86542290a2d","_cell_guid":"74ffb1ed-27b1-48c6-8ffc-18ca8b96ea41","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:20:51.866844Z","iopub.execute_input":"2025-06-05T16:20:51.867093Z","iopub.status.idle":"2025-06-05T16:20:58.521602Z","shell.execute_reply.started":"2025-06-05T16:20:51.867070Z","shell.execute_reply":"2025-06-05T16:20:58.521060Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MODEL_NAME = \"RN101\"\nINDEX_DIR = \"/kaggle/input/landmark-retrieval-2021-small/landmark-retrieval-2021/index\"\nTEST_DIR = \"/kaggle/input/landmark-retrieval-2021-small/landmark-retrieval-2021/test\"","metadata":{"_uuid":"46d4b956-872d-45ce-b889-3e7056ccf2a0","_cell_guid":"4b2343ac-a41c-4e8c-b449-243824d45681","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:20:58.522209Z","iopub.execute_input":"2025-06-05T16:20:58.522545Z","iopub.status.idle":"2025-06-05T16:20:58.526438Z","shell.execute_reply.started":"2025-06-05T16:20:58.522508Z","shell.execute_reply":"2025-06-05T16:20:58.525539Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel, preprocess = torch.hub.load(\"openai/CLIP\", MODEL_NAME, jit=False)\nmodel = model.eval().to(device)","metadata":{"_uuid":"867ca028-7176-40b6-bd59-5d115797bf53","_cell_guid":"92587c4c-a5c9-4483-a7d9-dc1fdb83a6f6","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:20:58.528127Z","iopub.execute_input":"2025-06-05T16:20:58.528335Z","iopub.status.idle":"2025-06-05T16:21:31.706207Z","shell.execute_reply.started":"2025-06-05T16:20:58.528318Z","shell.execute_reply":"2025-06-05T16:21:31.705599Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## CLIP Embedding","metadata":{"_uuid":"73e0f930-678e-4a35-a59a-2e4f13064d9d","_cell_guid":"bbb3994d-8de9-4005-a302-3db5c8b2291a","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"def encode_folder(root_dir, batch_size=256, desc=\"Encoding\"):\n    ds = datasets.ImageFolder(root_dir, transform=preprocess)\n    loader = DataLoader(\n        ds,\n        batch_size=batch_size,\n        shuffle=False,\n        num_workers=8,\n        pin_memory=True,\n    )\n\n    all_feats = []\n    all_paths = []\n    with torch.no_grad():\n        for batch_idx, (imgs, _) in enumerate(tqdm(loader, desc=desc, total=len(loader))):\n            imgs = imgs.cuda(non_blocking=True)\n\n            feats = model.encode_image(imgs)\n            feats = feats / feats.norm(dim=-1, keepdim=True)\n\n            all_feats.append(feats.cpu())\n            start = batch_idx * batch_size\n            batch_paths = [p for p, _ in ds.samples[start : start + imgs.size(0)]]\n            all_paths.extend(batch_paths)\n\n    embeddings = torch.cat(all_feats, dim=0).numpy().astype(np.float32)\n    return embeddings, all_paths","metadata":{"_uuid":"efd29ab0-250d-4895-944f-ec09b8bbbaf6","_cell_guid":"b0b0f11e-4386-43e8-b24d-818128e08c15","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:21:31.706819Z","iopub.execute_input":"2025-06-05T16:21:31.707025Z","iopub.status.idle":"2025-06-05T16:21:31.713425Z","shell.execute_reply.started":"2025-06-05T16:21:31.707008Z","shell.execute_reply":"2025-06-05T16:21:31.712639Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"index_embeds, index_img_paths = encode_folder(root_dir=INDEX_DIR, batch_size=256, desc=\"Index\")\ntest_embeds, test_img_paths = encode_folder(root_dir=TEST_DIR, batch_size=256, desc=\"Test\")\n\nprint(\"Index embeds:\", index_embeds.shape)\nprint(\"Test embeds: \", test_embeds.shape)","metadata":{"_uuid":"e399fbf5-7356-4430-8f75-a43c2c2e18db","_cell_guid":"f92480ee-e577-4d51-82ca-91f456f80c22","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:21:31.714217Z","iopub.execute_input":"2025-06-05T16:21:31.714499Z","iopub.status.idle":"2025-06-05T16:23:34.162418Z","shell.execute_reply.started":"2025-06-05T16:21:31.714483Z","shell.execute_reply":"2025-06-05T16:23:34.161357Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"np.save(\"index_embeds.npy\", index_embeds)\nnp.save(\"test_embeds.npy\", test_embeds)","metadata":{"_uuid":"06735b3d-bce2-4a06-ba23-6150ae63506c","_cell_guid":"ae38d603-c1de-42c4-a25a-f052ca9d72de","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:23:34.164296Z","iopub.execute_input":"2025-06-05T16:23:34.164590Z","iopub.status.idle":"2025-06-05T16:23:34.191763Z","shell.execute_reply.started":"2025-06-05T16:23:34.164543Z","shell.execute_reply":"2025-06-05T16:23:34.191248Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## FAISS Retrieval","metadata":{"_uuid":"fb601889-e2a5-4b3c-864e-c630424b7639","_cell_guid":"a300ec93-2565-4a98-9fe5-bfce5bb94b50","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"faiss.normalize_L2(index_embeds)\nfaiss.normalize_L2(test_embeds)\n\ndim = 512\nindex = faiss.IndexFlatIP(dim)\nindex.add(index_embeds)","metadata":{"_uuid":"60198ec7-9beb-4bde-b843-35853ebd4184","_cell_guid":"27d76b8c-6c86-4faf-aeef-5c015d154181","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:23:34.192553Z","iopub.execute_input":"2025-06-05T16:23:34.192818Z","iopub.status.idle":"2025-06-05T16:23:34.264330Z","shell.execute_reply.started":"2025-06-05T16:23:34.192792Z","shell.execute_reply":"2025-06-05T16:23:34.263788Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"K = 100\ndistances, indices = index.search(test_embeds, K)","metadata":{"_uuid":"d0ce96f9-b22b-4b2d-ac78-ed7a2dd20ef8","_cell_guid":"5cbf1d5a-dfc1-4720-b525-85a48d8b645f","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:23:34.265011Z","iopub.execute_input":"2025-06-05T16:23:34.265223Z","iopub.status.idle":"2025-06-05T16:23:34.384619Z","shell.execute_reply.started":"2025-06-05T16:23:34.265207Z","shell.execute_reply":"2025-06-05T16:23:34.383855Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"results = {}\nfor query_index in range(len(test_embeds)):\n    results[query_index] = [\n        {\n            \"candidate_index\": i,\n            \"global_score\": float(distances[query_index, rank_idx]),\n        }\n        for rank_idx, i in enumerate(indices[query_index])\n    ]","metadata":{"_uuid":"0c770e0e-d902-446f-bc9b-ebcaa6ef8c79","_cell_guid":"2c3f9cfd-2bf1-459c-b1e8-d8220ca66e62","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:23:34.387441Z","iopub.execute_input":"2025-06-05T16:23:34.387674Z","iopub.status.idle":"2025-06-05T16:23:59.550734Z","shell.execute_reply.started":"2025-06-05T16:23:34.387657Z","shell.execute_reply":"2025-06-05T16:23:59.549971Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Local Feature Extraction","metadata":{}},{"cell_type":"code","source":"backbone = model.visual\nreturn_nodes = {'layer2': 'feat'}\nvisual_fx = create_feature_extractor(backbone, return_nodes).eval().to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-05T16:23:59.551697Z","iopub.execute_input":"2025-06-05T16:23:59.552166Z","iopub.status.idle":"2025-06-05T16:23:59.840342Z","shell.execute_reply.started":"2025-06-05T16:23:59.552139Z","shell.execute_reply":"2025-06-05T16:23:59.839791Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def encode_images(paths, bs=64):\n    feats = []\n    with torch.no_grad():\n        for i in range(0, len(paths), bs):\n            imgs = [preprocess(Image.open(p).convert(\"RGB\")) for p in paths[i:i+bs]]\n            imgs = torch.stack(imgs).to(device, non_blocking=True)\n            out  = visual_fx(imgs)['feat']\n            feats.append(out)\n    return torch.cat(feats, dim=0)    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-05T16:23:59.840994Z","iopub.execute_input":"2025-06-05T16:23:59.841202Z","iopub.status.idle":"2025-06-05T16:23:59.846029Z","shell.execute_reply.started":"2025-06-05T16:23:59.841179Z","shell.execute_reply":"2025-06-05T16:23:59.845350Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def cosine_soft_inliers(q_local, c_locals, tau=0.02):\n    q = F.normalize(q_local,  p=2, dim=1)\n    c = F.normalize(c_locals, p=2, dim=2)\n    sim = torch.einsum('qc,btc->bqt', q, c)\n    weights = torch.softmax(sim / tau, dim=2)\n    \n    return weights.max(dim=2).values.sum(dim=1)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-05T16:23:59.846669Z","iopub.execute_input":"2025-06-05T16:23:59.846956Z","iopub.status.idle":"2025-06-05T16:23:59.857583Z","shell.execute_reply.started":"2025-06-05T16:23:59.846939Z","shell.execute_reply":"2025-06-05T16:23:59.857013Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def cosine_inlier_count(q_local, c_locals, threshold):\n    q = F.normalize(q_local,  p=2, dim=1)\n    c = F.normalize(c_locals, p=2, dim=2)\n\n    sim = torch.einsum('qc,btc->bqt', q, c)\n    best = sim.max(dim=2).values\n    inliers = (best > threshold).sum(dim=1)\n\n    return inliers","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for q_idx, cand_list in tqdm(results.items(),\n                             total=len(results),\n                             desc=\"Local Feature\"):\n\n    # ---- query --------------------------------------------------------\n    q_feat  = encode_images([test_img_paths[q_idx]], bs=1).squeeze(0)  # (C,H,W)\n    C, H, W = q_feat.shape\n    q_flat  = q_feat.permute(1, 2, 0).reshape(H * W, C)                # (Tq,C)\n\n    # ---- candidates ---------------------------------------------------\n    cand_paths = [index_img_paths[c['candidate_index']] for c in cand_list]\n    c_feats    = encode_images(cand_paths, bs=len(cand_paths))         # (B,C,H,W)\n    B, C, H, W = c_feats.shape\n    cand_flat  = c_feats.permute(0, 2, 3, 1).reshape(B, H * W, C)      # (B,Tc,C)\n\n    # ---- scoring ------------------------------------------------------\n    scores = cosine_soft_inliers(q_flat, cand_flat)\n\n    for entry, s in zip(cand_list, scores.cpu().tolist()):\n        entry[\"local_score\"] = s","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-05T16:23:59.858254Z","iopub.execute_input":"2025-06-05T16:23:59.858486Z","iopub.status.idle":"2025-06-05T16:44:27.871689Z","shell.execute_reply.started":"2025-06-05T16:23:59.858466Z","shell.execute_reply":"2025-06-05T16:44:27.870818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for q_idx, cand_list in results.items():\n    cand_list.sort(key=lambda d: d['global_score'], reverse=True)\n    for rank, item in enumerate(cand_list, start=1):\n        item['global_rank'] = rank\n\n    cand_list.sort(key=lambda d: d['local_score'], reverse=True)\n    for rank, item in enumerate(cand_list, start=1):\n        item['local_rank'] = rank","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-05T16:58:46.875414Z","iopub.execute_input":"2025-06-05T16:58:46.875705Z","iopub.status.idle":"2025-06-05T16:58:46.931043Z","shell.execute_reply.started":"2025-06-05T16:58:46.875684Z","shell.execute_reply":"2025-06-05T16:58:46.930518Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Evaluation","metadata":{"_uuid":"2977a5bd-abf9-4321-bec6-48c7faef7701","_cell_guid":"b93620af-3b41-45a7-a66d-21d22ba1bc4b","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"index_image_to_landmark = pd.read_csv(\"/kaggle/input/landmark-retrieval-2021-small/landmark-retrieval-2021/index_image_to_landmark.csv\").set_index(\"id\")\nrecognition_solution = pd.read_csv(\"/kaggle/input/landmark-retrieval-2021-small/landmark-retrieval-2021/recognition_solution.csv\").set_index(\"id\")\nretrieval_solution = pd.read_csv(\"/kaggle/input/landmark-retrieval-2021-small/landmark-retrieval-2021/retrieval_solution.csv\").set_index(\"id\")","metadata":{"_uuid":"434faa7d-c43c-436f-be04-3b2df6389589","_cell_guid":"51ec5d2d-2ebf-4f39-a005-f7f67227058f","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T16:44:51.686077Z","iopub.execute_input":"2025-06-05T16:44:51.686376Z","iopub.status.idle":"2025-06-05T16:44:51.851769Z","shell.execute_reply.started":"2025-06-05T16:44:51.686355Z","shell.execute_reply":"2025-06-05T16:44:51.851008Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"rows = []\n\nfor query_index, candidate_list in results.items():\n    query_path = test_img_paths[query_index]\n    query_id = os.path.basename(query_path)[-20:-4]\n    query_labels = str(recognition_solution['landmarks'].get(query_id, '')).split(' ')\n\n    for candidate in candidate_list:\n    \n        candidate_index = candidate['candidate_index']\n        candidate_path = index_img_paths[candidate_index]\n        candidate_id   = os.path.basename(candidate_path)[-20:-4]\n        candidate_label = str(index_image_to_landmark['landmark_id'].get(candidate_id, ''))\n        \n        rows.append({\n            'query_id':        query_id,\n            'query_labels':    query_labels,\n            'candidate_id':    candidate_id,\n            'candidate_label': candidate_label,\n            'global_score':    candidate['global_score'],\n            'local_score':     candidate['local_score'],\n            'global_rank':     candidate['global_rank'],\n            'local_rank':      candidate['local_rank']\n        })\n\nreranked_results = pd.DataFrame(rows)","metadata":{"_uuid":"8b265c1d-71bb-46e9-93aa-dd622bc85d1f","_cell_guid":"7c7f88da-fe12-4593-b68c-c59b898263ea","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T17:00:19.643315Z","iopub.execute_input":"2025-06-05T17:00:19.643597Z","iopub.status.idle":"2025-06-05T17:00:20.711642Z","shell.execute_reply.started":"2025-06-05T17:00:19.643576Z","shell.execute_reply":"2025-06-05T17:00:20.711091Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"reranked_results.head(10)","metadata":{"_uuid":"ed749ab2-f266-4eef-9f61-6f6dda11ecef","_cell_guid":"1ed0306b-5c98-4577-8aee-b97be7403ce6","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T17:00:21.693269Z","iopub.execute_input":"2025-06-05T17:00:21.693546Z","iopub.status.idle":"2025-06-05T17:00:21.705992Z","shell.execute_reply.started":"2025-06-05T17:00:21.693524Z","shell.execute_reply":"2025-06-05T17:00:21.705253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def topk_precision(df, rank=, k=5):\n    top_k = df[df[rank] <= k]\n    correct = 0\n\n    for _, row in top_k.iterrows():\n        query_labels = row['query_labels']\n        candidate = row['candidate_label']\n        \n        if candidate in query_labels:\n            correct += 1\n\n    total = len(top_k)\n    return correct / total if total > 0 else 0.0","metadata":{"_uuid":"31daf7f9-708f-45fa-9589-6fe197f885d0","_cell_guid":"47e58f1f-16b6-4e68-b30e-b9d2ea20d484","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T17:00:32.371232Z","iopub.execute_input":"2025-06-05T17:00:32.371505Z","iopub.status.idle":"2025-06-05T17:00:32.376212Z","shell.execute_reply.started":"2025-06-05T17:00:32.371485Z","shell.execute_reply":"2025-06-05T17:00:32.375394Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"k = 5\nrank = 'global_rank'\ntopk_prec = topk_precision(reranked_results, rank=rank, k=k)\nprint(f\"{rank.upper()} Top {k} Precision: {topk_prec:.4f}\")","metadata":{"_uuid":"4f2eb87b-88ac-44c4-9d31-ce4903be7f23","_cell_guid":"b8d789fb-d6fb-47b5-8e63-9ab6e3931fd2","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T17:00:33.030497Z","iopub.execute_input":"2025-06-05T17:00:33.030748Z","iopub.status.idle":"2025-06-05T17:00:33.249440Z","shell.execute_reply.started":"2025-06-05T17:00:33.030729Z","shell.execute_reply":"2025-06-05T17:00:33.248580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"k = 5\nrank = 'local_rank'\ntopk_prec = topk_precision(reranked_results, rank=rank, k=k)\nprint(f\"{rank.upper()} Top {k} Precision: {topk_prec:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-05T17:00:34.662006Z","iopub.execute_input":"2025-06-05T17:00:34.662861Z","iopub.status.idle":"2025-06-05T17:00:34.885884Z","shell.execute_reply.started":"2025-06-05T17:00:34.662829Z","shell.execute_reply":"2025-06-05T17:00:34.885081Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"k = 1\nrank = 'local_rank'\ntopk_prec = topk_precision(reranked_results, rank=rank, k=k)\nprint(f\"{rank.upper()} Top {k} Precision: {topk_prec:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-05T17:49:37.809906Z","iopub.execute_input":"2025-06-05T17:49:37.810733Z","iopub.status.idle":"2025-06-05T17:49:37.868677Z","shell.execute_reply.started":"2025-06-05T17:49:37.810707Z","shell.execute_reply":"2025-06-05T17:49:37.867994Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualization","metadata":{"_uuid":"d143e528-346a-4cb4-b060-b537e3529c2f","_cell_guid":"ebcafa5b-426f-442a-b81a-331fcf31bc15","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom PIL import Image\nfrom operator import itemgetter\n\ndef show_retrieval_results(\n        query_index: int,\n        candidate_list: list,          # results[query_index]\n        test_img_paths: list,\n        index_img_paths: list,\n        rank_by: str,  # field used to rank / sort\n        top_k: int = 5,\n        higher_better: bool = True):   # set False if lower rank means better\n    \"\"\"\n    Visualise a query and its top-k candidates, ranked by `rank_by`.\n\n    Parameters\n    ----------\n    rank_by        : str   column to sort on ('global_score', 'fused_score', 'local_score', etc.)\n    higher_better  : bool  if True, larger values rank higher (default for scores);\n                            set False to rank by e.g. 'global_rank' where 1 is best.\n    \"\"\"\n\n    # ---------- sort a copy so we don't touch original list ----------\n    sorted_cands = sorted(\n        candidate_list,\n        key=itemgetter(rank_by),\n        reverse=higher_better      # True ⇒ descending\n    )\n\n    # ---------- query image ------------------------------------------\n    query_path = test_img_paths[query_index]\n    query_img  = Image.open(query_path)\n    query_id   = query_path[-20:-4]\n    query_labels = str(recognition_solution['landmarks'].get(query_id, '')).split(' ')\n\n    plt.figure(figsize=(3*(top_k+1), 3))\n\n    plt.subplot(1, top_k + 1, 1)\n    plt.imshow(query_img)\n    plt.title(f\"Query\\nID: {query_labels}\", fontsize=10)\n    plt.axis(\"off\")\n\n    # ---------- candidate images -------------------------------------\n    for rank, item in enumerate(sorted_cands[:top_k], 1):\n        cand_idx  = item[\"candidate_index\"]\n        cand_path = index_img_paths[cand_idx]\n        cand_img  = Image.open(cand_path)\n        cand_id   = cand_path[-20:-4]\n        cand_label = str(index_image_to_landmark['landmark_id'].get(cand_id, ''))\n\n        score_val = item.get(rank_by, float(\"nan\"))\n        plt.subplot(1, top_k + 1, rank + 1)\n        plt.imshow(cand_img)\n        plt.title(f\"Rank {rank}\\n{rank_by}={score_val:.3f}\\nID:{cand_label}\",\n                  fontsize=9)\n        plt.axis(\"off\")\n\n    plt.tight_layout()\n    plt.show()\n","metadata":{"_uuid":"173ec811-cbb5-41ae-b608-130f56a4abf3","_cell_guid":"0effba6d-1813-4bea-a3c7-c11b3e457181","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T17:12:24.510403Z","iopub.execute_input":"2025-06-05T17:12:24.510708Z","iopub.status.idle":"2025-06-05T17:12:24.519248Z","shell.execute_reply.started":"2025-06-05T17:12:24.510686Z","shell.execute_reply":"2025-06-05T17:12:24.518613Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 1. Show top-5 by fused_score (bigger = better)\nshow_retrieval_results(0, results[0],\n                       test_img_paths, index_img_paths,\n                       rank_by=\"local_score\", higher_better=True)\n\n# 2. Show top-5 by global_rank (where 1 is best ⇒ lower is better)\nshow_retrieval_results(0, results[0],\n                       test_img_paths, index_img_paths,\n                       rank_by=\"global_score\", higher_better=True)","metadata":{"_uuid":"1042badd-4de4-4efb-98ae-092891545482","_cell_guid":"edd8eb2c-49dc-4391-a745-ed113ee47d80","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2025-06-05T17:16:15.476988Z","iopub.execute_input":"2025-06-05T17:16:15.477311Z","iopub.status.idle":"2025-06-05T17:16:16.959142Z","shell.execute_reply.started":"2025-06-05T17:16:15.477290Z","shell.execute_reply":"2025-06-05T17:16:16.958365Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}