{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.10.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":91451,"databundleVersionId":11223220,"sourceType":"competition"}],"dockerImageVersionId":30918,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 🐾 AnimalCLEF2025 Competition: Official Starter notebook\n\nThe **Goal of the** [AnimalCLEF2025](https://www.kaggle.com/competitions/animal-clef-2025/) competition is to identify individual animal (lynxes, salamanders and sea turtles) 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). The dataset is split into the database and query sets. For each image from the query set, the goal is to:\n\n- Predict whether the depicted individual is in the database.\n- If no, the prediction is `new_individual`.\n- If yes, the prediction should be the same as the individual in the database.","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":{"execution":{"iopub.status.busy":"2025-02-28T16:18:13.120141Z","iopub.execute_input":"2025-02-28T16:18:13.120497Z","iopub.status.idle":"2025-02-28T16:18:13.1244Z","shell.execute_reply.started":"2025-02-28T16:18:13.120467Z","shell.execute_reply":"2025-02-28T16:18:13.12338Z"}}},{"cell_type":"code","source":"!pip install git+https://github.com/WildlifeDatasets/wildlife-datasets@develop\n!pip install git+https://github.com/WildlifeDatasets/wildlife-tools","metadata":{"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-05-06T01:10:42.773001Z","iopub.execute_input":"2025-05-06T01:10:42.773247Z","iopub.status.idle":"2025-05-06T01:11:21.285047Z","shell.execute_reply.started":"2025-05-06T01:10:42.773213Z","shell.execute_reply":"2025-05-06T01:11:21.283980Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Dependencies import\nWe load all the required packages and then define the function `create_sample_submission`, which converts provided predictions and a submission file for the competition.","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport timm\nimport torchvision.transforms as T\nfrom wildlife_datasets.datasets import AnimalCLEF2025\nfrom wildlife_tools.features import DeepFeatures\nfrom wildlife_tools.similarity import CosineSimilarity\ndef create_sample_submission(dataset_query, predictions, file_name='sample_submission.csv'):\n    df = pd.DataFrame({\n        'image_id': dataset_query.metadata['image_id'],\n        'identity': predictions\n    })\n    df.to_csv(file_name, index=False)","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:11:21.286350Z","iopub.execute_input":"2025-05-06T01:11:21.286693Z","iopub.status.idle":"2025-05-06T01:11:50.074189Z","shell.execute_reply.started":"2025-05-06T01:11:21.286650Z","shell.execute_reply":"2025-05-06T01:11:50.073541Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"We need to specify the `root`, where the data are stored and then two image transformations. \n1. The first transform only resizes the images and is used for visualization.\n2. The second transform also converts it to torch tensor and is used for operations on neural networks.","metadata":{}},{"cell_type":"code","source":"root = '/kaggle/input/animal-clef-2025'\ntransform_display = T.Compose([\n    T.Resize([384, 384]),\n    ])\ntransform = T.Compose([\n    *transform_display.transforms,\n    T.ToTensor(),\n    T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225))\n    ])","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:11:50.075746Z","iopub.execute_input":"2025-05-06T01:11:50.076638Z","iopub.status.idle":"2025-05-06T01:11:50.081172Z","shell.execute_reply.started":"2025-05-06T01:11:50.076574Z","shell.execute_reply":"2025-05-06T01:11:50.080369Z"},"trusted":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. The following code specify which `transform` we want to use while loading images and that we want to load labels alongside images.","metadata":{}},{"cell_type":"code","source":"dataset = AnimalCLEF2025(root, transform=transform_display, load_label=True)","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:11:50.082778Z","iopub.execute_input":"2025-05-06T01:11:50.083055Z","iopub.status.idle":"2025-05-06T01:12:19.416735Z","shell.execute_reply.started":"2025-05-06T01:11:50.083035Z","shell.execute_reply":"2025-05-06T01:12:19.416035Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"📌 **Plotting a sample grid** of the data. We can see that all animals in the testing sets are turtles. The column `dataset` states that all the photos come from the SeaTurtleID2022 dataset.","metadata":{}},{"cell_type":"code","source":"dataset.plot_grid()\ndataset.metadata","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:12:19.417492Z","iopub.execute_input":"2025-05-06T01:12:19.417748Z","iopub.status.idle":"2025-05-06T01:12:20.523300Z","shell.execute_reply.started":"2025-05-06T01:12:19.417727Z","shell.execute_reply":"2025-05-06T01:12:20.522347Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"This is confirmed by showing all datasets from which the data is composed of.","metadata":{}},{"cell_type":"code","source":"dataset.metadata[['dataset', 'split']].value_counts()","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:12:20.524208Z","iopub.execute_input":"2025-05-06T01:12:20.524426Z","iopub.status.idle":"2025-05-06T01:12:20.538113Z","shell.execute_reply.started":"2025-05-06T01:12:20.524408Z","shell.execute_reply":"2025-05-06T01:12:20.537357Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Now, we can plot the data of any dataset. Here we do it for the SeaTurtleID2022. The images are the same as in the query set. The goal for each image from the query set will be to determine its identity from the database (it will always be in the form `SeaTurtleID2022_t???` or to decide that it is a new individual (it is not present in the database).","metadata":{}},{"cell_type":"code","source":"idx = dataset.metadata['identity'].str.startswith('SeaTurtleID2022')\nidx[idx.isnull()] = False\ndataset.plot_grid(idx=idx);","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:12:20.538853Z","iopub.execute_input":"2025-05-06T01:12:20.539068Z","iopub.status.idle":"2025-05-06T01:12:21.066698Z","shell.execute_reply.started":"2025-05-06T01:12:20.539051Z","shell.execute_reply":"2025-05-06T01:12:21.065852Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference with MegaDescriptor\n\nInstead of training a classifier, we can just use out of the shelf pretrained models - [MegaDescriptor](https://huggingface.co/BVRA/MegaDescriptor-L-384). We use MegaDescriptor to extract features from all images. \n\n**Note:** _It is highly recommended to use the GPU acceleration._","metadata":{}},{"cell_type":"code","source":"# Loading the dataset\ndataset = AnimalCLEF2025(root, transform=transform, load_label=True)\ndataset_database = dataset.get_subset(dataset.metadata['split'] == 'database')\ndataset_query = dataset.get_subset(dataset.metadata['split'] == 'query')\nn_query = len(dataset_query)","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:12:21.068974Z","iopub.execute_input":"2025-05-06T01:12:21.069329Z","iopub.status.idle":"2025-05-06T01:12:27.698972Z","shell.execute_reply.started":"2025-05-06T01:12:21.069274Z","shell.execute_reply":"2025-05-06T01:12:27.697980Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Loading the model\nname = 'hf-hub:BVRA/MegaDescriptor-L-384'\ndevice = 'cuda'\nmodel = timm.create_model(name, num_classes=0, pretrained=True)\nextractor = DeepFeatures(model, device=device, batch_size=32, num_workers=0)\nfeatures_database = extractor(dataset_database)\nfeatures_query = extractor(dataset_query)","metadata":{"_kg_hide-input":false,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-05-06T01:12:27.700078Z","iopub.execute_input":"2025-05-06T01:12:27.700350Z","iopub.status.idle":"2025-05-06T01:38:50.310961Z","shell.execute_reply.started":"2025-05-06T01:12:27.700318Z","shell.execute_reply":"2025-05-06T01:38:50.310068Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Here we extract the similarity between the images. The similarity equals to the cosine similarity between the corresponding feature vectors. 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.","metadata":{}},{"cell_type":"code","source":"similarity = CosineSimilarity()(features_query, features_database)","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:47:11.700705Z","iopub.execute_input":"2025-05-06T01:47:11.701209Z","iopub.status.idle":"2025-05-06T01:47:12.159524Z","shell.execute_reply.started":"2025-05-06T01:47:11.701165Z","shell.execute_reply":"2025-05-06T01:47:12.158866Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"For each query image we extract the closest image as predicted by MegaDescriptor. Their indices are stored in `pred_idx` and the similarity score of the top match in `pred_scores`.","metadata":{}},{"cell_type":"code","source":"pred_idx = similarity.argsort(axis=1)[:,-1]\npred_scores = similarity[range(n_query), pred_idx]","metadata":{"execution":{"iopub.status.busy":"2025-05-06T01:47:20.458443Z","iopub.execute_input":"2025-05-06T01:47:20.458794Z","iopub.status.idle":"2025-05-06T01:47:21.246708Z","shell.execute_reply.started":"2025-05-06T01:47:20.458768Z","shell.execute_reply":"2025-05-06T01:47:21.245987Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"This is sufficient to perform predictions as shown below in `predictions`. However, we also need to predict whether the individual is new. We utilize the simplest idea and predict that the individual is new whenever the similarity score of the top prediction is below a certain threshold which we arbirtarily selected as 0.6. We create a submission file, which scores 30.0% on the public leaderboard.","metadata":{}},{"cell_type":"code","source":"new_individual = 'new_individual'\nthreshold = 0.6\nlabels = dataset_database.labels_string\npredictions = labels[pred_idx]\npredictions[pred_scores < threshold] = new_individual\ncreate_sample_submission(dataset_query, predictions, file_name='sample_submission.csv')","metadata":{"execution":{"iopub.status.busy":"2025-05-06T02:00:30.155077Z","iopub.execute_input":"2025-05-06T02:00:30.155357Z","iopub.status.idle":"2025-05-06T02:00:30.164089Z","shell.execute_reply.started":"2025-05-06T02:00:30.155338Z","shell.execute_reply":"2025-05-06T02:00:30.163244Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}