{"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":[{"sourceId":127282,"databundleVersionId":15520105,"sourceType":"competition"}],"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\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":"## Dependencies instalation\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 / method 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,"execution":{"iopub.status.busy":"2026-02-04T16:15:30.984448Z","iopub.execute_input":"2026-02-04T16:15:30.985036Z","iopub.status.idle":"2026-02-04T16:15:58.492240Z","shell.execute_reply.started":"2026-02-04T16:15:30.985002Z","shell.execute_reply":"2026-02-04T16:15:58.491509Z"},"_kg_hide-input":false,"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dependencies import\n\nWe load all the required packages.","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 sklearn.cluster import DBSCAN\nfrom wildlife_datasets.datasets import AnimalCLEF2026\nfrom wildlife_tools.features import DeepFeatures\nfrom wildlife_tools.similarity import CosineSimilarity","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:15:58.494005Z","iopub.execute_input":"2026-02-04T16:15:58.494245Z","iopub.status.idle":"2026-02-04T16:16:33.381360Z","shell.execute_reply.started":"2026-02-04T16:15:58.494214Z","shell.execute_reply":"2026-02-04T16:16:33.380736Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 📊 Visualizing Data\n\nSince `AnimalCLEF2015` is the child class of `datasets.WildlifeDataset` from [wildlife-datasets](https://github.com/WildlifeDatasets/wildlife-datasets/blob/main/wildlife_datasets/datasets/datasets.py), it inherits all its methods and attributes. Its integral part is the `transform` attribute, which performs transforms on the input images (such as converting them to torch tensors). We start with no transform.","metadata":{}},{"cell_type":"code","source":"root = '/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)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:16:33.382290Z","iopub.execute_input":"2026-02-04T16:16:33.383059Z","iopub.status.idle":"2026-02-04T16:16:33.474491Z","shell.execute_reply.started":"2026-02-04T16:16:33.383031Z","shell.execute_reply":"2026-02-04T16:16:33.473890Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The important part of the `dataset` class is the DataFrame `metadata`, which contains information about individual images.","metadata":{}},{"cell_type":"code","source":"dataset_full.metadata.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:16:33.476144Z","iopub.execute_input":"2026-02-04T16:16:33.476436Z","iopub.status.idle":"2026-02-04T16:16:33.505824Z","shell.execute_reply.started":"2026-02-04T16:16:33.476410Z","shell.execute_reply":"2026-02-04T16:16:33.505165Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The column dataset shows into which of the two datasets the image belongs (see also the column species) and the split is either the train or the test set. We see that TexasHornedLizards does not have any train set, which corresponds to the real-world situation, where the database of images have been just created and needs to be divided into individuals.","metadata":{}},{"cell_type":"code","source":"dataset_full.metadata[['dataset', 'split']].value_counts(sort=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:16:33.506600Z","iopub.execute_input":"2026-02-04T16:16:33.506918Z","iopub.status.idle":"2026-02-04T16:16:33.525938Z","shell.execute_reply.started":"2026-02-04T16:16:33.506892Z","shell.execute_reply":"2026-02-04T16:16:33.525239Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"For this purpose of this notebook, we will focus only on the test set and ignore images from the training set. We also create `datasets` corresponding to each species.","metadata":{}},{"cell_type":"code","source":"dataset_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)\ndatasets","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:16:33.526832Z","iopub.execute_input":"2026-02-04T16:16:33.527136Z","iopub.status.idle":"2026-02-04T16:16:33.547865Z","shell.execute_reply.started":"2026-02-04T16:16:33.527106Z","shell.execute_reply":"2026-02-04T16:16:33.547242Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"📌 **Plotting a sample grid** of the data. We see that there is a large variability in sea turtles, which were taken underwater in their natural habitat. On the other hands, the images of lizards were taken in captivity.","metadata":{}},{"cell_type":"code","source":"for dataset in datasets.values():\n    dataset.plot_grid(n_rows=3, n_cols=4, rotate=False);","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:16:33.548761Z","iopub.execute_input":"2026-02-04T16:16:33.549333Z","iopub.status.idle":"2026-02-04T16:16:36.670914Z","shell.execute_reply.started":"2026-02-04T16:16:33.549307Z","shell.execute_reply":"2026-02-04T16:16:36.670247Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference with MegaDescriptor and MiewID\n\nInstead of training a classifier, we can just use out of the shelf pretrained models - [MegaDescriptor](https://huggingface.co/BVRA/MegaDescriptor-L-384) and [MiewID](https://huggingface.co/conservationxlabs/miewid-msv3). We use MegaDescriptor to extract features from sea turtles and MiewID for lizards and then cosine similarity to compute the similarity for each pair of images. Since the cosine similarity reflects the angle between the feature vectors, high similarity means that the feature vectors are close to each other and should depict the same individual.\n\n**Note:** It is highly recommended to use the GPU acceleration.","metadata":{}},{"cell_type":"code","source":"device = 'cuda'\nbatch_size = 32\n\nsimilarities = {}\nfor name, dataset in datasets.items():\n    # Select the model for feature extraction\n    if name in ['SalamanderID2025', 'SeaTurtleID2022']:\n        model = timm.create_model(\"hf-hub:BVRA/MegaDescriptor-L-384\", pretrained=True).eval()\n        size = 384\n    elif name in ['LynxID2025', 'TexasHornedLizards']:\n        model = AutoModel.from_pretrained('conservationxlabs/miewid-msv3', trust_remote_code=True)\n        size = 512\n    else:\n        raise ValueError('Name does not exist')\n\n    # Set the extractor and transform for the images\n    matcher = CosineSimilarity()\n    extractor = DeepFeatures(model=model, device=device, batch_size=batch_size)\n    transform = T.Compose([\n        T.Resize(size=(size, size)),\n        T.ToTensor(),\n        T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)),\n    ])\n\n    # Set the transform for images\n    dataset.set_transform(transform)\n    # Extract features\n    features = extractor(dataset)\n    # Compute the similarity matrix\n    similarity = matcher(features, features)\n    similarities[name] = similarity","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:16:36.671799Z","iopub.execute_input":"2026-02-04T16:16:36.672103Z","iopub.status.idle":"2026-02-04T16:19:43.807523Z","shell.execute_reply.started":"2026-02-04T16:16:36.672077Z","shell.execute_reply":"2026-02-04T16:19:43.806526Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"The next cell shows that TexasHornedLizards has 274 images. Each of the image has a feature vector of size 2152. The similarity matrix computes the similarity between each pair of images and therefore, it is symmetric and of size (274, 274).","metadata":{}},{"cell_type":"code","source":"print(f'Dataset {name} with {len(dataset)} images.')\nprint(f'Features have size {features.features.shape}.')\nprint(f'Similarity matrix has shape {similarity.shape}.')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:19:43.808913Z","iopub.execute_input":"2026-02-04T16:19:43.809209Z","iopub.status.idle":"2026-02-04T16:19:43.814802Z","shell.execute_reply.started":"2026-02-04T16:19:43.809175Z","shell.execute_reply":"2026-02-04T16:19:43.813983Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We intend to use the clustering algorithm DBSCAN on the similarity matrix. Since this algoritm return -1 for unclustered entries, we write a function `relabel_negatives`, which creates new clusters for these clusters (each new cluster with one element). For DBSCAN, we specify `min_samples=2` to allow for small clusters and select the epsilon parameters (determining the radius of the clusters) rather arbitrarily. Also due to DBSCAN requirements, we need to convert the similarity matrix into the distance matrix.","metadata":{}},{"cell_type":"code","source":"def 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\ndef run_DBSCAN(similarity, eps):\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 = DBSCAN(eps=eps, metric='precomputed', min_samples=2)\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-02-04T16:19:43.817963Z","iopub.execute_input":"2026-02-04T16:19:43.818226Z","iopub.status.idle":"2026-02-04T16:19:43.834383Z","shell.execute_reply.started":"2026-02-04T16:19:43.818201Z","shell.execute_reply":"2026-02-04T16:19:43.833539Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Now we run DBSCAN for all extracted similarity matrices and save the results. We select the parameter eps rather arbitrarily.","metadata":{}},{"cell_type":"code","source":"results = None\neps_opt = {\n    'LynxID2025': 0.3,\n    'SalamanderID2025': 0.2,\n    'SeaTurtleID2022': 0.4,\n    'TexasHornedLizards': 0.24,\n}\nfor name, similarity in similarities.items():\n    # Save the clusters for one dataset\n    clusters = run_DBSCAN(similarity, eps_opt[name])\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.to_csv('submission.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:19:43.835275Z","iopub.execute_input":"2026-02-04T16:19:43.835499Z","iopub.status.idle":"2026-02-04T16:19:43.893030Z","shell.execute_reply.started":"2026-02-04T16:19:43.835465Z","shell.execute_reply":"2026-02-04T16:19:43.892335Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualizing parameter eps\n\nWe can run DBSCAN for multiple values or eps and visualize the number of clusters. Small eps correspond to 274 clusters (each cluster is one images) and large eps to 1 cluster (all images are in one cluster)","metadata":{}},{"cell_type":"code","source":"eps_all = np.linspace(0.00001, 1, 500)\nn_clusters = []\nfor eps in eps_all:\n    clusters = run_DBSCAN(similarity, eps)\n    n_clusters.append(len(np.unique(clusters)))\n\nplt.plot(eps_all, n_clusters)\nplt.vlines(eps_opt[name], min(n_clusters), max(n_clusters), linestyles='dotted')\nplt.title('number of clusters')\nplt.xlabel('eps (minimum cluster distance for DBSCAN)');","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:19:43.894033Z","iopub.execute_input":"2026-02-04T16:19:43.894434Z","iopub.status.idle":"2026-02-04T16:19:45.681435Z","shell.execute_reply.started":"2026-02-04T16:19:43.894406Z","shell.execute_reply":"2026-02-04T16:19:45.680379Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We can also inspect the images manually. Plotting the clusters shows that most of the clusters are correct (but ofr example  the last plotted cluster is wrong).","metadata":{}},{"cell_type":"code","source":"clusters = run_DBSCAN(similarity, eps_opt[name])\nn_plot = 5\ni_plot = pd.Series(clusters).value_counts().index\nfor i in i_plot[:n_plot]:\n    dataset.plot_grid(idx = clusters == i, n_cols=3, img_min=500, rotate=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-04T16:19:45.682615Z","iopub.execute_input":"2026-02-04T16:19:45.683207Z","iopub.status.idle":"2026-02-04T16:19:47.164510Z","shell.execute_reply.started":"2026-02-04T16:19:45.683181Z","shell.execute_reply":"2026-02-04T16:19:47.163744Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Possible improvements\n\nThere are many possible improvements to investigate. We list some of them:\n\n* Use the training set, for example for validating the performance of hyperparameter selection.\n* Use automatic tools for data preprocessing such as image segmentation.\n* Replace MegaDescriptor or MiewID with another model. It is possible to use their combination, local feature models such as SuperPoint or any other.\n* Replace DBSCAN by a method which is not so sensitive to its eps hyperparameters, for example HDBSCAN.","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}