{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceType":"competition","sourceId":127282,"databundleVersionId":15520105}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🐾 AnimalCLEF2026 Competition: Official Starter notebook 2\n\nThe **Goal of the** [AnimalCLEF2026](https://www.kaggle.com/competitions/animal-clef-2026/) competition is to cluster individual animal (loggerhead sea turtles and Texas horned lizards) in photos. This notebook visualize the provided dataset and propose a baseline solution, based on the state-of-the-art re-identification model [MegaDescriptor](https://huggingface.co/BVRA/MegaDescriptor-L-384). It also suggests some possible improvements for the participants to follow.\n\nThe dataset is split into the training and test sets. For each image from the test set, the goal is to assing in a cluster. Each cluster should correspond to one individual animal. The cluster name must start with `cluster_SeaTurtleID2022_` or `cluster_TexasHornedLizards_`.\n\n![](https://www.googleapis.com/download/storage/v1/b/kaggle-user-content/o/inbox%2F12294787%2Ff015de35bcf462f36f74faabb20dd7e6%2FAnimalCLEF2026.png?generation=1767855188505985&alt=media)","metadata":{}},{"cell_type":"markdown","source":"## Continuation of the Starter notebook 1\n\nThis notebook is a continuation of the previous [starter notebook](https://www.kaggle.com/code/picekl/animalclef2026-starter-notebook). We assume that the reader has gone through the previous notebook. Therefore, we do not go through introductory sections such as plotting; instead, we summarise only the necessary functions and variables from the previous notebook.\n\nFor the competition, we provide two Python packages for loading and preprocessing of available datasets ([wildlife-datasets](https://github.com/WildlifeDatasets/wildlife-datasets)) and tools/methods for animal re-identification ([wildlife-tools](https://github.com/WildlifeDatasets/wildlife-tools)).","metadata":{}},{"cell_type":"code","source":"!pip install wildlife-datasets git+https://github.com/WildlifeDatasets/wildlife-tools --quiet --upgrade-strategy only-if-needed","metadata":{"trusted":true,"_kg_hide-input":false,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2026-04-05T09:20:03.580925Z","iopub.execute_input":"2026-04-05T09:20:03.581581Z","iopub.status.idle":"2026-04-05T09:20:30.734666Z","shell.execute_reply.started":"2026-04-05T09:20:03.581551Z","shell.execute_reply":"2026-04-05T09:20:30.733908Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We load now all the required functions and variables from the previous notebook.","metadata":{}},{"cell_type":"code","source":"import warnings\nwarnings.filterwarnings(\"ignore\")\nimport numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport torchvision.transforms as T\nimport timm\nfrom transformers import AutoModel\nfrom wildlife_datasets.datasets import AnimalCLEF2026\nfrom wildlife_tools.features import DeepFeatures\nfrom wildlife_tools.similarity import CosineSimilarity\n\ndef relabel_negatives(labels):\n    labels = np.array(labels)\n    neg_indices = np.where(labels == -1)[0]\n    new_labels = np.arange(labels.max()+1, labels.max()+1+len(neg_indices))\n    labels[neg_indices] = new_labels\n    return labels\n\nroot = '/kaggle/input/animal-clef-2026'\ndataset_full = AnimalCLEF2026(\n    root,\n    transform=None,\n    load_label=True,\n    factorize_label=True,\n    check_files=False\n)\ndataset_full = dataset_full.get_subset(dataset_full.df['split'] == 'test')\n\ndatasets = {}\nfor name in dataset_full.metadata['dataset'].unique():\n    datasets[name] = dataset_full.get_subset(dataset_full.df['dataset'] == name)","metadata":{"trusted":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2026-04-05T09:20:30.736202Z","iopub.execute_input":"2026-04-05T09:20:30.736521Z","iopub.status.idle":"2026-04-05T09:21:05.292396Z","shell.execute_reply.started":"2026-04-05T09:20:30.736489Z","shell.execute_reply":"2026-04-05T09:21:05.291738Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Difficulties in the previous notebook\n\nThe main deficiency of the previous notebook was the need for manual tuning, namely:\n\n- Which method to use for which dataset. It used [MegaDescriptor](https://huggingface.co/BVRA/MegaDescriptor-L-384) for two datasets and [MiewID](https://huggingface.co/conservationxlabs/miewid-msv3) for two datasets.\n- How to select the `eps` parameter for `DBSCAN`, which clusters the similarity scores. Setting `eps=1` produced a single large cluster containing all images, while `eps=0` produced a separate cluster for each image. The optimal value depended on the dataset and was difficult to tune.\n\nThis notebook removes both of these difficulties.","metadata":{}},{"cell_type":"markdown","source":"## Merging predictions via WildFusion\n\n[WildFusion](https://link.springer.com/chapter/10.1007/978-3-031-92387-6_2) is a technique to merge predictions from different models. It divides the models into two categories:\n\n- The *priority matcher* is a fast model, which selects `B` candidates to be evaluated by the pipeline models.\n- The *pipeline matchers* evaluate the predictions proposed by the priority matcher. Each pipeline model is calibrated into the interval $[0, 1]$ and the resulting similarity score is then the average of all pipeline scores.\n\nThe idea of the pipeline is to reduce the number of evaluations from $D\\times Q$, where $D$ is the size of the database and $Q$ is the size of the query set, to $B\\times Q$, which is significantly smaller, especially for large databases.\n\nThe next code defines the pipelines of MegaDeescriptor and MiewID. Since the output is cosine similarity for both pipelines, the outputs are normalised, and we skip calibration. We point out `cache_path`, a new addition to WildlifeTools that saves the computed features in an LMDB database. Therefore, they do not need to be recomputed when an additional experiment is performed.","metadata":{}},{"cell_type":"code","source":"from wildlife_tools.similarity.wildfusion import SimilarityPipeline, WildFusion\n\ndevice = 'cuda'\nbatch_size = 32\n\npipeline_mega = SimilarityPipeline(\n    matcher = CosineSimilarity(),\n    extractor = DeepFeatures(\n        model = timm.create_model(\"hf-hub:BVRA/MegaDescriptor-L-384\", pretrained=True).eval(),\n        device=device,\n        batch_size=batch_size,\n        cache_path=\"features/mega\"\n    ),\n    transform = T.Compose([\n        T.Resize(size=(384, 384)),\n        T.ToTensor(),\n        T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ]),\n    calibration = None\n)\npipeline_miew =  SimilarityPipeline(\n    matcher = CosineSimilarity(),\n    extractor = DeepFeatures(\n        model = AutoModel.from_pretrained('conservationxlabs/miewid-msv3', trust_remote_code=True),\n        device=device,\n        batch_size=batch_size,\n        cache_path=\"features/miew\"\n    ),\n    transform = T.Compose([\n        T.Resize(size=(512, 512)),\n        T.ToTensor(),\n        T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ]),\n    calibration = None\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T07:34:33.592553Z","iopub.execute_input":"2026-04-05T07:34:33.593171Z","iopub.status.idle":"2026-04-05T07:34:39.753284Z","shell.execute_reply.started":"2026-04-05T07:34:33.593144Z","shell.execute_reply":"2026-04-05T07:34:39.752365Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We then run WildFusion for each dataset separately. Since we select `B` to be equal to the whole dataset, the priority matcher does not apply, and the result is only the average of the MegaDescriptor and MiewID scores. However, we decided to keep this structure for two generalisation options:\n\n- The pipeline may include slow matchers such as ALIKED, DISK or SuperPoint. Then `B` should be selected much lower.\n- The pipelines are currently uncalibrated. The calibration may be performed on the training set and normalise the cosine similarity output $[-1, 1]$ to the standard interval $[0, 1]$ using a *non-linear* mapping. The current form is ready for adding calibration as described [here](https://wildlifedatasets.github.io/wildlife-tools/wildfusion/) or [here](https://github.com/WildlifeDatasets/wildlife-tools/blob/main/examples/example-wildfusion.ipynb).","metadata":{}},{"cell_type":"code","source":"pipelines = [pipeline_mega, pipeline_miew]\n\nsimilarities = {}\nfor name, dataset in datasets.items():\n    B = len(dataset)\n    wildfusion = WildFusion(calibrated_pipelines = pipelines, priority_pipeline = pipeline_mega)\n    similarity = wildfusion(dataset, dataset, B=B)    \n    similarities[name] = similarity.astype(np.float64)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T07:40:45.185569Z","iopub.execute_input":"2026-04-05T07:40:45.186368Z","iopub.status.idle":"2026-04-05T07:40:58.161638Z","shell.execute_reply.started":"2026-04-05T07:40:45.186339Z","shell.execute_reply":"2026-04-05T07:40:58.160876Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Removing DBSCAN\n\nTo replace DBSCAN, especially its tuning parameter `eps`, we use its hierarchical version, [HDBSCAN](https://scikit-learn.org/stable/modules/generated/sklearn.cluster.HDBSCAN.html). We first replace the `run_DBSCAN` function from the previous notebook with `run_HDBSCAN`. Note that we kept the `min_cluster_size=2` keyword in `HDBSCAN` to allow for small clusters.","metadata":{}},{"cell_type":"code","source":"from sklearn.cluster import HDBSCAN\n\ndef run_HDBSCAN(similarity):\n    # Convert similarity (high is good) to distance (small is good)\n    distance = (np.max(similarity) - np.maximum(similarity, 0)) / np.max(similarity)\n    # Obtain predictions\n    clustering = HDBSCAN(min_cluster_size=2, metric='precomputed')\n    clusters = clustering.fit(distance)\n    # Relabel -1 clusters into separate clusters\n    return relabel_negatives(clusters.labels_)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T07:41:03.292909Z","iopub.execute_input":"2026-04-05T07:41:03.293247Z","iopub.status.idle":"2026-04-05T07:41:03.298499Z","shell.execute_reply.started":"2026-04-05T07:41:03.293222Z","shell.execute_reply":"2026-04-05T07:41:03.297624Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We then use the same code to compute the predictions.","metadata":{}},{"cell_type":"code","source":"results = pd.DataFrame()\nfor name, similarity in similarities.items():\n    # Save the clusters for one dataset\n    clusters = run_HDBSCAN(similarity)\n    result = pd.DataFrame({\n        'image_id': datasets[name].metadata['image_id'],\n        'cluster': [f'cluster_{name}_{c}' for c in clusters]\n    })\n    # Merge the clusters to other datasets\n    results = pd.concat((results, result))\nresults = results.reset_index(drop=True)\nresults.to_csv('submission.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-05T07:41:05.162443Z","iopub.execute_input":"2026-04-05T07:41:05.162732Z","iopub.status.idle":"2026-04-05T07:41:05.311479Z","shell.execute_reply.started":"2026-04-05T07:41:05.162709Z","shell.execute_reply":"2026-04-05T07:41:05.310622Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Summary\n\nThis technique slightly improves the performance (from 19.4% to 20.3%). However, it removes the need to tune the pesky hyperparameter `eps` manually and is ready for additional generalisations such as:\n\n- Replace MegaDescriptor or MiewID with other models, such as the local feature model SuperPoint or any other.\n- Replace the uncalibrated models with their calibrated versions. This will be necessary when using a local feature model, which returns the number of matching keypoints, which is a much higher score than the cosine similarity.\n\nAdditional improvements may be achieved by:\n\n- Use automatic tools for data preprocessing, such as image segmentation.","metadata":{}}]}