{"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\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-04-04T09:52:32.542581Z","iopub.execute_input":"2026-04-04T09:52:32.542870Z","iopub.status.idle":"2026-04-04T09:53:01.388681Z","shell.execute_reply.started":"2026-04-04T09:52:32.542836Z","shell.execute_reply":"2026-04-04T09:53:01.387819Z"},"_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":"!pip install lmdb","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T09:53:47.293434Z","iopub.execute_input":"2026-04-04T09:53:47.293775Z","iopub.status.idle":"2026-04-04T09:53:51.272134Z","shell.execute_reply.started":"2026-04-04T09:53:47.293739Z","shell.execute_reply":"2026-04-04T09:53:51.271416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import warnings\nimport hdbscan\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-04-04T09:53:53.711613Z","iopub.execute_input":"2026-04-04T09:53:53.711957Z","iopub.status.idle":"2026-04-04T09:54:30.853173Z","shell.execute_reply.started":"2026-04-04T09:53:53.711923Z","shell.execute_reply":"2026-04-04T09:54:30.852560Z"},"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn.functional as F\n\ndef extract_features(dataset, model, extractor, tta=False):\n    features = extractor(dataset).features  # (N, D)\n\n    features = torch.tensor(features)\n\n    # 🔥 нормализация (очень важно)\n    features = F.normalize(features, dim=1)\n\n    if tta:\n        # простая TTA: flip\n        features_flip = F.normalize(features.clone(), dim=1)\n        features = (features + features_flip) / 2\n        features = F.normalize(features, dim=1)\n\n    return features.numpy()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T09:54:58.828933Z","iopub.execute_input":"2026-04-04T09:54:58.830201Z","iopub.status.idle":"2026-04-04T09:54:58.835369Z","shell.execute_reply.started":"2026-04-04T09:54:58.830160Z","shell.execute_reply":"2026-04-04T09:54:58.834601Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def cosine_similarity_matrix(features):\n    return features @ features.T","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T09:55:00.601686Z","iopub.execute_input":"2026-04-04T09:55:00.602333Z","iopub.status.idle":"2026-04-04T09:55:00.606714Z","shell.execute_reply.started":"2026-04-04T09:55:00.602291Z","shell.execute_reply":"2026-04-04T09:55:00.605734Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def sim_to_distance(sim):\n    sim = np.clip(sim, -1, 1)\n    return 1 - sim  # 🔥 стандарт для cosine","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T09:55:03.303093Z","iopub.execute_input":"2026-04-04T09:55:03.303402Z","iopub.status.idle":"2026-04-04T09:55:03.307818Z","shell.execute_reply.started":"2026-04-04T09:55:03.303368Z","shell.execute_reply":"2026-04-04T09:55:03.307041Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.cluster import DBSCAN\n\ndef run_dbscan(similarity, eps):\n    distance = sim_to_distance(similarity)\n\n    clustering = DBSCAN(\n        eps=eps,\n        min_samples=2,\n        metric='precomputed',\n        n_jobs=-1\n    ).fit(distance)\n\n    return relabel_negatives(clustering.labels_)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T09:55:11.366779Z","iopub.execute_input":"2026-04-04T09:55:11.367725Z","iopub.status.idle":"2026-04-04T09:55:11.373727Z","shell.execute_reply.started":"2026-04-04T09:55:11.367673Z","shell.execute_reply":"2026-04-04T09:55:11.372915Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def clean_clusters(labels, min_size=2):\n    labels = np.array(labels)\n    unique = np.unique(labels)\n\n    new_label = labels.max() + 1\n\n    for u in unique:\n        idx = np.where(labels == u)[0]\n        if len(idx) < min_size:\n            for i in idx:\n                labels[i] = new_label\n                new_label += 1\n\n    return labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T09:55:13.199358Z","iopub.execute_input":"2026-04-04T09:55:13.199673Z","iopub.status.idle":"2026-04-04T09:55:13.204848Z","shell.execute_reply.started":"2026-04-04T09:55:13.199646Z","shell.execute_reply":"2026-04-04T09:55:13.204005Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\n\ndef relabel_negatives(labels):\n    labels = np.array(labels)\n\n    # находим все -1 (noise в DBSCAN)\n    neg_indices = np.where(labels == -1)[0]\n\n    if len(neg_indices) == 0:\n        return labels\n\n    # создаём новые уникальные кластеры\n    max_label = labels.max()\n    new_labels = np.arange(max_label + 1, max_label + 1 + len(neg_indices))\n\n    labels[neg_indices] = new_labels\n    return labels","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T09:56:26.396615Z","iopub.execute_input":"2026-04-04T09:56:26.396946Z","iopub.status.idle":"2026-04-04T09:56:26.401873Z","shell.execute_reply.started":"2026-04-04T09:56:26.396917Z","shell.execute_reply":"2026-04-04T09:56:26.401043Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# === LOAD DATASET ===\nroot = '/kaggle/input/animal-clef-2026'\n\ndataset_full = AnimalCLEF2026(\n    root,\n    transform=None,\n    load_label=True,\n    factorize_label=True,\n    check_files=False\n)\n\n# берём только test\ndataset_full = dataset_full.get_subset(dataset_full.df['split'] == 'test')\n\n# разбиваем по видам\ndatasets = {}\nfor name in dataset_full.metadata['dataset'].unique():\n    datasets[name] = dataset_full.get_subset(\n        dataset_full.df['dataset'] == name\n    )\n\nprint(\"Datasets:\", datasets.keys())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T09:56:48.164160Z","iopub.execute_input":"2026-04-04T09:56:48.164765Z","iopub.status.idle":"2026-04-04T09:56:48.265804Z","shell.execute_reply.started":"2026-04-04T09:56:48.164735Z","shell.execute_reply":"2026-04-04T09:56:48.264991Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport hdbscan\n\ndef run_hdbscan(similarity, min_cluster_size=2):\n    # similarity → distance\n    distance = 1 - np.clip(similarity, -1, 1)\n\n    # 🔥 КЛЮЧЕВОЙ ФИКС\n    distance = distance.astype(np.float64)\n\n    clusterer = hdbscan.HDBSCAN(\n        min_cluster_size=min_cluster_size,\n        metric='precomputed',\n        cluster_selection_method='eom'\n    )\n\n    labels = clusterer.fit_predict(distance)\n\n    return relabel_negatives(labels)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T10:03:30.348049Z","iopub.execute_input":"2026-04-04T10:03:30.348413Z","iopub.status.idle":"2026-04-04T10:03:30.354532Z","shell.execute_reply.started":"2026-04-04T10:03:30.348378Z","shell.execute_reply":"2026-04-04T10:03:30.353738Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = 'cuda'\nbatch_size = 32\n\nresults = None\n\neps_opt = {\n    'LynxID2025': 0.3,\n    'SalamanderID2025': 0.2,\n    'SeaTurtleID2022': 0.4,\n    'TexasHornedLizards': 0.24,\n}\n\nresults = None\n\nfor name, dataset in datasets.items():\n\n    # === MODEL ===\n    if name in ['SalamanderID2025', 'SeaTurtleID2022']:\n        model = timm.create_model(\n            \"hf-hub:BVRA/MegaDescriptor-L-384\",\n            pretrained=True\n        ).eval()\n        size = 384\n    else:\n        model = AutoModel.from_pretrained(\n            'conservationxlabs/miewid-msv3',\n            trust_remote_code=True\n        )\n        size = 512\n\n    # === TRANSFORM ===\n    transform = T.Compose([\n        T.Resize((size, size)),\n        T.ToTensor(),\n        T.Normalize(mean=(0.485, 0.456, 0.406),\n                    std=(0.229, 0.224, 0.225)),\n    ])\n    dataset.set_transform(transform)\n\n    # === FEATURES ===\n    extractor = DeepFeatures(\n        model=model,\n        device=device,\n        batch_size=batch_size\n    )\n\n    features = extract_features(dataset, model, extractor, tta=True)\n\n    # === SIMILARITY ===\n    similarity = cosine_similarity_matrix(features)\n\n    # === 🔥 HDBSCAN ===\n    clusters = run_hdbscan(similarity, min_cluster_size=2)\n\n    # === CLEAN ===\n    clusters = clean_clusters(clusters)\n\n    # === SAVE ===\n    result = pd.DataFrame({\n        'image_id': dataset.metadata['image_id'],\n        'cluster': [f'cluster_{name}_{c}' for c in clusters]\n    })\n\n    results = pd.concat((results, result))\n\nresults.to_csv('submission.csv', index=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-04T10:03:32.708066Z","iopub.execute_input":"2026-04-04T10:03:32.708772Z","iopub.status.idle":"2026-04-04T10:05:52.616634Z","shell.execute_reply.started":"2026-04-04T10:03:32.708742Z","shell.execute_reply":"2026-04-04T10:05:52.615670Z"}},"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":"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}]}