{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"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.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":127282,"databundleVersionId":15520105,"isSourceIdPinned":false}],"dockerImageVersionId":31329,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"c843e5ca","cell_type":"markdown","source":"## Segmenting Salamanders with SAM3 and YOLOE-26L-SEG\n\nExcluding the background information is a preprocessing step that can make our re-identification pipeline robust. Segmentation models like SAM3 and YOLOE26 provide a low-barrier entry point to this.","metadata":{}},{"id":"f546dbd0","cell_type":"code","source":"# You will probably need to restart the kernel after installing ultralytics\n!uv pip install ultralytics","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:21:59.603219Z","iopub.execute_input":"2026-03-21T17:21:59.603558Z","iopub.status.idle":"2026-03-21T17:21:59.861611Z","shell.execute_reply.started":"2026-03-21T17:21:59.603529Z","shell.execute_reply":"2026-03-21T17:21:59.860941Z"}},"outputs":[],"execution_count":null},{"id":"17c58373","cell_type":"code","source":"# Check if GPU is available and show GPU information\n!nvidia-smi","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:21:59.863220Z","iopub.execute_input":"2026-03-21T17:21:59.863444Z","iopub.status.idle":"2026-03-21T17:22:00.325235Z","shell.execute_reply.started":"2026-03-21T17:21:59.863419Z","shell.execute_reply":"2026-03-21T17:22:00.324307Z"}},"outputs":[],"execution_count":null},{"id":"b8fa076f","cell_type":"code","source":"# Paths of directories containing the unprocessed images\nimport os\nfrom pathlib import Path\nsalamander_test_dir = Path(\"/kaggle/input/competitions/animal-clef-2026/images/SalamanderID2025/test\")\nsalamander_train_dir = Path(\"/kaggle/input/competitions/animal-clef-2026/images/SalamanderID2025/train\")\nlen(list(salamander_test_dir.iterdir())), len(list(salamander_train_dir.iterdir()))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:22:00.326438Z","iopub.execute_input":"2026-03-21T17:22:00.326697Z","iopub.status.idle":"2026-03-21T17:22:00.404508Z","shell.execute_reply.started":"2026-03-21T17:22:00.326643Z","shell.execute_reply":"2026-03-21T17:22:00.403931Z"}},"outputs":[],"execution_count":null},{"id":"055b05a5","cell_type":"code","source":"from ultralytics import YOLO\nmodel = YOLO(\"yoloe-26l-seg.pt\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:22:00.406211Z","iopub.execute_input":"2026-03-21T17:22:00.406426Z","iopub.status.idle":"2026-03-21T17:22:15.176993Z","shell.execute_reply.started":"2026-03-21T17:22:00.406405Z","shell.execute_reply":"2026-03-21T17:22:15.176349Z"}},"outputs":[],"execution_count":null},{"id":"36cb497b","cell_type":"markdown","source":"## Salamander vibe-check","metadata":{}},{"id":"e2941e32","cell_type":"code","source":"model.set_classes([\"salamander\"])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:22:15.177806Z","iopub.execute_input":"2026-03-21T17:22:15.178215Z","iopub.status.idle":"2026-03-21T17:22:21.284493Z","shell.execute_reply.started":"2026-03-21T17:22:15.178182Z","shell.execute_reply":"2026-03-21T17:22:21.283579Z"}},"outputs":[],"execution_count":null},{"id":"b79657eb","cell_type":"code","source":"from PIL import Image\nsalamander_image_path = salamander_train_dir / \"10/25a3a8049b5aaed6_307.jpg\"\nsalamander_image = Image.open(salamander_image_path)\nsalamander_image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:22:21.285531Z","iopub.execute_input":"2026-03-21T17:22:21.285821Z","iopub.status.idle":"2026-03-21T17:22:21.629076Z","shell.execute_reply.started":"2026-03-21T17:22:21.285795Z","shell.execute_reply":"2026-03-21T17:22:21.628121Z"}},"outputs":[],"execution_count":null},{"id":"08bee580","cell_type":"code","source":"\nsegmented_salamander = model.predict(salamander_image)\nsegmented_salamander[0].show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:22:21.630368Z","iopub.execute_input":"2026-03-21T17:22:21.630778Z","iopub.status.idle":"2026-03-21T17:22:25.399633Z","shell.execute_reply.started":"2026-03-21T17:22:21.630728Z","shell.execute_reply":"2026-03-21T17:22:25.398926Z"}},"outputs":[],"execution_count":null},{"id":"8f67fde2","cell_type":"markdown","source":"## Sample output with different confidence thresholds\n\nLowering the confidence threshold increases recall at the expense of precision. We have more pixels drawn into the mask, but also more false positives.","metadata":{}},{"id":"32ad2aa2","cell_type":"code","source":"# Effect of lowering confidence threshold: compare 0.25 vs 0.15 on the same sampled images\nimport random\nimport matplotlib.pyplot as plt\nfrom pathlib import Path\n\nBASE_CONF = 0.25\nLOWER_CONF = 0.10\nSAMPLES_PER_SPLIT = 5\n\n# Reuse existing sampled lists if available; otherwise rebuild safely\nif \"sampled_train\" not in globals() or \"sampled_test\" not in globals():\n    def collect_image_paths(root: Path):\n        valid_ext = {\".jpg\", \".jpeg\", \".png\", \".bmp\", \".webp\"}\n        return sorted([p for p in root.rglob(\"*\") if p.is_file() and p.suffix.lower() in valid_ext])\n\n    rng_local = random.Random(42)\n    train_images_local = collect_image_paths(salamander_train_dir)\n    test_images_local = collect_image_paths(salamander_test_dir)\n    sampled_train = rng_local.sample(train_images_local, k=min(SAMPLES_PER_SPLIT, len(train_images_local))) if train_images_local else []\n    sampled_test = rng_local.sample(test_images_local, k=min(SAMPLES_PER_SPLIT, len(test_images_local))) if test_images_local else []\n\nif len(sampled_train) == 0 and len(sampled_test) == 0:\n    raise ValueError(\"No sampled salamander images available for threshold comparison.\")\n\nfig, axes = plt.subplots(4, SAMPLES_PER_SPLIT, figsize=(22, 16))\nfor ax in axes.flatten():\n    ax.axis(\"off\")\n\n\ndef render_row(images, row_index, split_name, conf_value):\n    positive_panels = 0\n    for col in range(SAMPLES_PER_SPLIT):\n        ax = axes[row_index, col]\n        if col >= len(images):\n            ax.set_title(f\"{split_name} @ {conf_value}: (no sample)\", fontsize=9)\n            continue\n\n        image_path = images[col]\n        result = model.predict(source=str(image_path), conf=conf_value, verbose=False)[0]\n        overlay_bgr = result.plot()\n        overlay_rgb = overlay_bgr[..., ::-1]\n        mask_count = len(result.masks) if result.masks is not None else 0\n        positive_panels += int(mask_count > 0)\n\n        ax.imshow(overlay_rgb)\n        ax.set_title(f\"{split_name} @ {conf_value}: {image_path.name}\\nmasks={mask_count}\", fontsize=9)\n        ax.axis(\"off\")\n\n    return positive_panels, len(images)\n\n\ntrain_base_pos, train_total = render_row(sampled_train, row_index=0, split_name=\"Train\", conf_value=BASE_CONF)\ntrain_low_pos, _ = render_row(sampled_train, row_index=1, split_name=\"Train\", conf_value=LOWER_CONF)\ntest_base_pos, test_total = render_row(sampled_test, row_index=2, split_name=\"Test\", conf_value=BASE_CONF)\ntest_low_pos, _ = render_row(sampled_test, row_index=3, split_name=\"Test\", conf_value=LOWER_CONF)\n\nfig.suptitle(\"Threshold effect on salamander segmentation (same images)\", fontsize=14)\nplt.tight_layout()\nplt.show()\n\nprint(f\"Train masks>0 panels: conf={BASE_CONF} -> {train_base_pos}/{train_total}, conf={LOWER_CONF} -> {train_low_pos}/{train_total}\")\nprint(f\"Test masks>0 panels: conf={BASE_CONF} -> {test_base_pos}/{test_total}, conf={LOWER_CONF} -> {test_low_pos}/{test_total}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:22:25.400878Z","iopub.execute_input":"2026-03-21T17:22:25.401139Z","iopub.status.idle":"2026-03-21T17:22:42.618015Z","shell.execute_reply.started":"2026-03-21T17:22:25.401103Z","shell.execute_reply":"2026-03-21T17:22:42.617099Z"}},"outputs":[],"execution_count":null},{"id":"fad69d00","cell_type":"markdown","source":"## SAM3 \n\nThe SAM3 model provides better precision and recall at our random sample. Tradeoff: it has a higher parameter count than YOLOE26 and is slower at inference at time. \n\nYou will need to accept Meta's license and set up a HuggingFace token to access the model https://huggingface.co/facebook/sam3 ","metadata":{}},{"id":"54eae806","cell_type":"code","source":"from kaggle_secrets import UserSecretsClient\nuser_secrets = UserSecretsClient()\nos.environ[\"HF_TOKEN\"] = user_secrets.get_secret(\"hf_api\")                                                                                                                                                                                                                                                             ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:23:43.842159Z","iopub.execute_input":"2026-03-21T17:23:43.842811Z","iopub.status.idle":"2026-03-21T17:23:43.916430Z","shell.execute_reply.started":"2026-03-21T17:23:43.842777Z","shell.execute_reply":"2026-03-21T17:23:43.915892Z"}},"outputs":[],"execution_count":null},{"id":"6433574a","cell_type":"code","source":"from transformers import Sam3Processor, Sam3Model\nimport torch\nfrom PIL import Image\nimport requests\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\nmodel = Sam3Model.from_pretrained(\"facebook/sam3\").to(device)\nprocessor = Sam3Processor.from_pretrained(\"facebook/sam3\")\n\n# Segment using text prompt\ninputs = processor(images=salamander_image, text=\"salamander\", return_tensors=\"pt\").to(device)\n\nwith torch.no_grad():\n    outputs = model(**inputs)\n\n# Post-process results\nresults = processor.post_process_instance_segmentation(\n    outputs,\n    threshold=0.5,\n    mask_threshold=0.5,\n    target_sizes=inputs.get(\"original_sizes\").tolist()\n)[0]\n\nprint(f\"Found {len(results['masks'])} objects\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:23:43.917605Z","iopub.execute_input":"2026-03-21T17:23:43.917892Z","iopub.status.idle":"2026-03-21T17:24:27.476729Z","shell.execute_reply.started":"2026-03-21T17:23:43.917864Z","shell.execute_reply":"2026-03-21T17:24:27.475862Z"}},"outputs":[],"execution_count":null},{"id":"6411678d","cell_type":"code","source":"import matplotlib \nimport numpy as np\n\ndef overlay_masks(image, masks):\n    image = image.convert(\"RGBA\")\n    masks = 255 * masks.cpu().numpy().astype(np.uint8)\n    \n    n_masks = masks.shape[0]\n    cmap = matplotlib.colormaps.get_cmap(\"rainbow\").resampled(n_masks)\n    colors = [\n        tuple(int(c * 255) for c in cmap(i)[:3])\n        for i in range(n_masks)\n    ]\n\n    for mask, color in zip(masks, colors):\n        mask = Image.fromarray(mask)\n        overlay = Image.new(\"RGBA\", image.size, color + (0,))\n        alpha = mask.point(lambda v: int(v * 0.5))\n        overlay.putalpha(alpha)\n        image = Image.alpha_composite(image, overlay)\n    return image\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:24:27.477881Z","iopub.execute_input":"2026-03-21T17:24:27.478420Z","iopub.status.idle":"2026-03-21T17:24:27.485013Z","shell.execute_reply.started":"2026-03-21T17:24:27.478389Z","shell.execute_reply":"2026-03-21T17:24:27.484262Z"}},"outputs":[],"execution_count":null},{"id":"d4d4d649","cell_type":"code","source":"overlay_masks(salamander_image, results[\"masks\"])\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:24:27.486332Z","iopub.execute_input":"2026-03-21T17:24:27.486809Z","iopub.status.idle":"2026-03-21T17:24:30.696512Z","shell.execute_reply.started":"2026-03-21T17:24:27.486782Z","shell.execute_reply":"2026-03-21T17:24:30.695352Z"}},"outputs":[],"execution_count":null},{"id":"2bc456f9","cell_type":"code","source":"def collect_image_paths(root: Path):\n    \"\"\"Recursively collect image files from a directory.\"\"\"\n    valid_ext = {\".jpg\", \".jpeg\", \".png\", \".bmp\", \".webp\"}\n    return sorted([p for p in root.rglob(\"*\") if p.is_file() and p.suffix.lower() in valid_ext])\n\n\ndef render_sam3_grid(image_paths, text_prompt, thresholds, title, cols=5):\n    \"\"\"Render a grid: rows = thresholds, columns = sampled images.\n\n    Each panel shows the SAM3 overlay + mask count annotation.\n    \"\"\"\n    n_images = min(cols, len(image_paths))\n    n_rows = len(thresholds)\n\n    fig, axes = plt.subplots(n_rows, n_images, figsize=(4.4 * n_images, 4 * n_rows))\n    if n_rows == 1:\n        axes = axes[np.newaxis, :]\n    if n_images == 1:\n        axes = axes[:, np.newaxis]\n\n    for ax in axes.flatten():\n        ax.axis(\"off\")\n\n    positive_counts = {t: 0 for t in thresholds}\n\n    for row_idx, thresh in enumerate(thresholds):\n        for col_idx in range(n_images):\n            ax = axes[row_idx, col_idx]\n            img_path = image_paths[col_idx]\n            image = Image.open(img_path).convert(\"RGB\")\n\n            masks, boxes, scores = segment_with_sam3(image, text_prompt, threshold=thresh)\n            mask_count = len(masks) if masks is not None else 0\n            positive_counts[thresh] += int(mask_count > 0)\n\n            if masks is not None:\n                overlay = overlay_masks(image, masks)\n            else:\n                overlay = image\n\n            ax.imshow(overlay)\n            ax.set_title(f\"thresh={thresh}: {img_path.name}\\nmasks={mask_count}\", fontsize=9)\n            ax.axis(\"off\")\n\n    fig.suptitle(title, fontsize=14)\n    plt.tight_layout()\n    plt.show()\n\n    for thresh in thresholds:\n        print(f\"  thresh={thresh}: {positive_counts[thresh]}/{n_images} images with >=1 mask\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:24:30.697583Z","iopub.execute_input":"2026-03-21T17:24:30.697854Z","iopub.status.idle":"2026-03-21T17:24:30.706961Z","shell.execute_reply.started":"2026-03-21T17:24:30.697827Z","shell.execute_reply":"2026-03-21T17:24:30.706331Z"}},"outputs":[],"execution_count":null},{"id":"63742f74","cell_type":"code","source":"def segment_with_sam3(image, text_prompt, threshold=0.5, mask_threshold=0.5):\n    \"\"\"Run SAM3 text-prompted segmentation on a single image.\n\n    Returns (masks, boxes, scores) or (None, None, None) if no objects found.\n    \"\"\"\n    inputs = processor(images=image, text=text_prompt, return_tensors=\"pt\").to(device)\n\n    with torch.no_grad():\n        outputs = model(**inputs)\n\n    results = processor.post_process_instance_segmentation(\n        outputs,\n        threshold=threshold,\n        mask_threshold=mask_threshold,\n        target_sizes=inputs.get(\"original_sizes\").tolist(),\n    )[0]\n\n    masks = results.get(\"masks\")\n    boxes = results.get(\"boxes\")\n    scores = results.get(\"scores\")\n\n    if masks is None or len(masks) == 0:\n        return None, None, None\n    return masks, boxes, scores","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:24:30.707862Z","iopub.execute_input":"2026-03-21T17:24:30.708309Z","iopub.status.idle":"2026-03-21T17:24:30.723992Z","shell.execute_reply.started":"2026-03-21T17:24:30.708276Z","shell.execute_reply":"2026-03-21T17:24:30.723456Z"}},"outputs":[],"execution_count":null},{"id":"6970b9d9","cell_type":"code","source":"SAMPLES_PER_SPLIT = 5\nTHRESHOLDS = [0.5, 0.3]\nrng = random.Random(42)\n\n# Collect and sample salamander images\ntrain_images = collect_image_paths(salamander_train_dir)\ntest_images = collect_image_paths(salamander_test_dir)\n\nsampled_train = rng.sample(train_images, k=min(SAMPLES_PER_SPLIT, len(train_images))) if train_images else []\nsampled_test = rng.sample(test_images, k=min(SAMPLES_PER_SPLIT, len(test_images))) if test_images else []\nsampled_salamanders = sampled_train + sampled_test\n\nprint(f\"Sampled {len(sampled_train)} train + {len(sampled_test)} test = {len(sampled_salamanders)} salamander images\")\n\nif sampled_salamanders:\n    render_sam3_grid(\n        sampled_salamanders,\n        text_prompt=\"salamander\",\n        thresholds=THRESHOLDS,\n        title=\"SAM3 Salamander Segmentation — threshold comparison\",\n        cols=len(sampled_salamanders),\n    )\nelse:\n    print(\"No salamander images found — skipping grid.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-21T17:24:30.724939Z","iopub.execute_input":"2026-03-21T17:24:30.725210Z","iopub.status.idle":"2026-03-21T17:25:24.861213Z","shell.execute_reply.started":"2026-03-21T17:24:30.725185Z","shell.execute_reply":"2026-03-21T17:25:24.860016Z"}},"outputs":[],"execution_count":null}]}