{"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":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-02-05T13:56:11.979425Z","iopub.execute_input":"2026-02-05T13:56:11.979700Z","iopub.status.idle":"2026-02-05T13:56:24.866499Z","shell.execute_reply.started":"2026-02-05T13:56:11.979668Z","shell.execute_reply":"2026-02-05T13:56:24.865788Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport glob\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torchvision import models, transforms\nfrom PIL import Image\nfrom sklearn.cluster import AgglomerativeClustering\nfrom sklearn.preprocessing import normalize\nfrom tqdm import tqdm\n\n# --- Configuration ---\nDATA_ROOT = '/kaggle/input/animal-clef-2026'\nOUTPUT_FILE = 'submission.csv'\nBATCH_SIZE = 32\nIMAGE_SIZE = 224\n# Tuning Tip: Adjust this threshold. \n# Lower = Stricter (more clusters), Higher = Looser (fewer clusters)\nCLUSTERING_THRESHOLD = 0.3  \n\n# --- 1. Setup Model (ResNet50) ---\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nprint(f\"🚀 Running on {device}\")\n\n# Load a pre-trained ResNet50\n# Note: In a real competition, you'd use the provided MegaDescriptor or a ReID specific model\nmodel = models.resnet50(pretrained=True)\n# Remove the final classification layer to get feature embeddings\nmodel.fc = nn.Identity()\nmodel = model.to(device)\nmodel.eval()\n\n# Define image preprocessing\ntransform = transforms.Compose([\n    transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], \n                         std=[0.229, 0.224, 0.225]),\n])\n\n# --- 2. Helper Functions ---\n\ndef load_image(path):\n    try:\n        img = Image.open(path).convert('RGB')\n        return transform(img)\n    except Exception as e:\n        print(f\"Error loading {path}: {e}\")\n        return torch.zeros((3, IMAGE_SIZE, IMAGE_SIZE))\n\ndef extract_features(image_paths):\n    embeddings = []\n    # Process in batches\n    for i in range(0, len(image_paths), BATCH_SIZE):\n        batch_paths = image_paths[i:i+BATCH_SIZE]\n        batch_imgs = [load_image(p) for p in batch_paths]\n        batch_tensor = torch.stack(batch_imgs).to(device)\n        \n        with torch.no_grad():\n            features = model(batch_tensor)\n            # Flatten if necessary\n            features = features.view(features.size(0), -1)\n            embeddings.append(features.cpu().numpy())\n            \n    return np.vstack(embeddings)\n\n# --- 3. Main Processing Loop ---\nspecies_list = ['LynxID2025', 'SalamanderID2025', 'SeaTurtleID2022', 'TexasHornedLizards']\nall_preds = []\n\nprint(\"Starting feature extraction and clustering...\")\n\nfor species in species_list:\n    print(f\"\\n--- Processing {species} ---\")\n    \n    # correct path for test images: images/{Species}/test/*.JPG\n    search_path = os.path.join(DATA_ROOT, 'images', species, 'test', '*.JPG')\n    # Note: Extension might be .jpg or .JPG, handle case sensitivity if needed\n    image_files = glob.glob(search_path)\n    \n    # Some files might have lowercase extensions\n    if not image_files:\n        search_path_lower = os.path.join(DATA_ROOT, 'images', species, 'test', '*.jpg')\n        image_files = glob.glob(search_path_lower)\n        \n    print(f\"Found {len(image_files)} images.\")\n    \n    if len(image_files) == 0:\n        continue\n        \n    # Extract IDs from filenames (assuming format 'hash_id.JPG')\n    # We need the 'image_id' which is usually the number after the underscore\n    # But wait, looking at sample_submission, the image_id is just an integer (e.g., 3, 5, 12)\n    # We need to match filenames to the metadata or sample submission image_ids.\n    \n    # Let's verify how to get image_id from filename.\n    # Usually metadata.csv maps 'path' to 'image_id'. Let's use metadata.\n    meta_df = pd.read_csv(os.path.join(DATA_ROOT, 'metadata.csv'))\n    # Filter for this species and test set\n    species_meta = meta_df[(meta_df['dataset'] == species) & (meta_df['split'] == 'test')].copy()\n    \n    # We will process strictly the images listed in metadata for this species\n    # Construct full paths\n    # The 'path' column in metadata usually looks like \"images/SeaTurtleID2022/test/xyz.JPG\"\n    # We need to prepend DATA_ROOT\n    species_meta['full_path'] = species_meta['path'].apply(lambda x: os.path.join(DATA_ROOT, x))\n    \n    valid_paths = species_meta['full_path'].tolist()\n    valid_ids = species_meta['image_id'].tolist()\n    \n    print(\"Extracting features...\")\n    features = extract_features(valid_paths)\n    \n    # Normalize features (Cosine Similarity requires normalized vectors)\n    features = normalize(features, norm='l2')\n    \n    print(\"Clustering...\")\n    # Agglomerative Clustering is standard for this.\n    # distance_threshold determines when to stop merging clusters.\n    clustering = AgglomerativeClustering(\n        n_clusters=None,\n        distance_threshold=CLUSTERING_THRESHOLD,\n        metric='cosine',\n        linkage='average'\n    )\n    \n    labels = clustering.fit_predict(features)\n    \n    # Format the predictions\n    # Cluster format: cluster_{Species}_{Label}\n    for img_id, label in zip(valid_ids, labels):\n        cluster_name = f\"cluster_{species}_{label}\"\n        all_preds.append({'image_id': img_id, 'cluster': cluster_name})\n\n# --- 4. Create Submission ---\npred_df = pd.DataFrame(all_preds)\n\n# Load sample submission to ensure correct order and rows\nsample_sub = pd.read_csv(os.path.join(DATA_ROOT, 'sample_submission.csv'))\n\n# Merge our predictions into the sample layout\nfinal_sub = sample_sub[['image_id']].merge(pred_df, on='image_id', how='left')\n\n# Fill missing (if any) with a unique singleton cluster\nmissing_mask = final_sub['cluster'].isna()\nif missing_mask.sum() > 0:\n    print(f\"Warning: {missing_mask.sum()} images missing predictions. Assigning singletons.\")\n    final_sub.loc[missing_mask, 'cluster'] = (\n        'cluster_Unknown_' + final_sub.loc[missing_mask, 'image_id'].astype(str)\n    )\n\nfinal_sub.to_csv(OUTPUT_FILE, index=False)\nprint(f\"\\n✅ Submission saved to {OUTPUT_FILE}\")\nprint(final_sub.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-05T13:58:21.420892Z","iopub.execute_input":"2026-02-05T13:58:21.421545Z","iopub.status.idle":"2026-02-05T13:59:43.661387Z","shell.execute_reply.started":"2026-02-05T13:58:21.421517Z","shell.execute_reply":"2026-02-05T13:59:43.660508Z"}},"outputs":[],"execution_count":null}]}