{
  "id": 679229,
  "title": "27th placement solution",
  "url": "/competitions/vesuvius-challenge-surface-detection/discussion/679229",
  "author_name": "Taha_Alshatiri",
  "post_date": "2026-02-28T02:23:13.580000",
  "votes": 14,
  "comment_count": 0,
  "views": 0,
  "content": "<p>thanks to kaggle and the host team for this great competition</p>\n<p>this is conclusion of our solution </p>\n<h1>Modeling and Ensembling</h1>\n<p>In the final days we used multiple combinations of these models( patch size = 160 for all of them)</p>\n<p>1)W-net (included in nnUnet framework better performance than nnUnet but slow in inference time which made the ensembling harder)</p>\n<p>2)Ukan( Unet based on kolmogorov arnold networks it performs well and very fast slightly lower performance than nnUnet but good for diversity)</p>\n<p>3)nnUNet default</p>\n<p>4)nnUNet MedialSurfaceRecall </p>\n<p>5) nnUNet CLDiceLoss</p>\n<p>6) nnUNet MedialTverskySurfaceRecall</p>\n<p>we tried many combinations for the ensemble and looking at the best notebook we have submitted it seems that it included 4 models\nworth noting that we tried to use different validation fold for each model to increase diversity and to not make all models ignore the fold 0 val data</p>\n<p>for the  ensemble logic we used adaptive merging made by <a href=\"https://www.kaggle.com/i2nfinit3y\" target=\"_blank\">@i2nfinit3y</a> </p>\n<pre><code>def adaptive_merge_probs(probs_list, window_size=9, detail_scale=1.0, smoothing=1.0):\n    preds = [p.astype(np.float32) for p in probs_list]\n\n    # 1. (Local Mean)\n    # uniform_filter 是极其高效的盒状滤波\n    local_means = [uniform_filter(pred, size=window_size) for pred in preds]\n\n    # 2. (Local Variance)\n    # Var = Mean((X - Mean)^2)\n    variances = [\n        uniform_filter((pred - mean) ** 2, size=window_size) * detail_scale\n        for pred, mean in zip(preds, local_means)\n    ]\n\n    # 3. (Smoothing)\n    if smoothing &gt; 0:\n        variances = [uniform_filter(var, size=int(window_size * smoothing)) for var in variances]\n\n    # 4. (Weights)\n    weights = [var / (var.max() + 1e-6) for var in variances]\n\n    weight_sum = sum(weights)\n    weight_sum = weight_sum + 1e-8\n\n    # 5. (Weighted Merge)\n    merged = sum(pred * weight for pred, weight in zip(preds, weights)) / weight_sum\n\n    return merged\n</code></pre>\n<p>also to ensure that the ensemble works fine  <a href=\"https://www.kaggle.com/i2nfinit3y\" target=\"_blank\">@i2nfinit3y</a>  made the  parrallization   code for the 2 T4s</p>\n<p>we also tried many approaches like 3Dino and  convnext 2.5d models,I have tried to use MedDinov3 which achieve SOTA in 2d medical segmentation where i introduced each 3 slices as RGB channels inspired by <a href=\"https://www.kaggle.com/tom99763\" target=\"_blank\">@tom99763</a> solution in the <a href=\"https://www.kaggle.com/competitions/rsna-intracranial-aneurysm-detection/writeups/9th-place-solution\" target=\"_blank\">RNSA</a> competition but it was bad and no where close to nnUnet</p>\n<h1>Postprocessing</h1>\n<p>this seemed to be the most important part yet the most counterproductive part, we tried many things like that were supposed to increase the lb but the opposite happened including holes closing, also I had an idea in the last 3 days were I though  about an algorithm that looks at the volume from top where it sees the upper edges of the papers and then go down from slice to slice, if 2 lines/components that are slightly parallel combined into a single one then it is  a wrong merge and it add a small cut, they should be parallel and not close to each other tips or else that is just the end of a hole/tunnel in a sheet that merge again, unfortunately I didn't have enough time to try and refine it specially that we had other experiments to try, but I thought that it was worth mentioning this is a  simple clauded sample  of the code which probably have problems and buggy</p>\n<pre><code>from skimage.morphology import skeletonize\nfrom scipy.ndimage import label as ndi_label, binary_dilation, convolve\nimport numpy as np\n\ndef get_endpoints(skel_comp):\n    \"\"\"Find tip pixels of a skeleton component — pixels with exactly 1 neighbor.\"\"\"\n    neighbor_count = convolve(\n        skel_comp.astype(np.uint8),\n        np.ones((3,3), dtype=np.uint8),\n        mode='constant'\n    )\n    # endpoint = skeleton pixel with exactly 2 neighbors (itself + 1 other)\n    endpoints = skel_comp &amp; (neighbor_count == 2)\n    return np.argwhere(endpoints)\n\n\ndef is_tip_to_tip(comp1, comp2, tip_fraction=0.2):\n    \"\"\"\n    Returns True if the closest approach between comp1 and comp2\n    is near the TIPS of both — meaning they are end-to-end (hole in single sheet).\n    Returns False if the closest approach is near their bodies — side-to-side (touching sheets).\n\n    tip_fraction: what fraction of the component length counts as \"tip region\"\n    \"\"\"\n    coords1 = np.argwhere(comp1)\n    coords2 = np.argwhere(comp2)\n\n    if len(coords1) == 0 or len(coords2) == 0:\n        return False\n\n    # Find the closest pair of points between the two components\n    # For efficiency: sample if too large\n    MAX_SAMPLE = 200\n    s1 = coords1[np.random.choice(len(coords1), min(MAX_SAMPLE, len(coords1)), replace=False)]\n    s2 = coords2[np.random.choice(len(coords2), min(MAX_SAMPLE, len(coords2)), replace=False)]\n\n    # Pairwise distances\n    diff = s1[:, None, :] - s2[None, :, :]   # (N1, N2, 2)\n    dists = np.sqrt((diff**2).sum(axis=2))    # (N1, N2)\n    min_idx = np.unravel_index(np.argmin(dists), dists.shape)\n    closest1 = s1[min_idx[0]]  # closest point on comp1\n    closest2 = s2[min_idx[1]]  # closest point on comp2\n\n    # Check if closest point on each component is near its tip\n    # by measuring how far it is from the component's endpoints\n    def near_tip(closest_pt, all_coords, fraction):\n        \"\"\"True if closest_pt is within fraction*length of either endpoint.\"\"\"\n        # endpoints = most extreme points along the principal axis\n        # simple proxy: just check distance to the two most distant points\n        dists_to_all = np.sqrt(((all_coords - closest_pt)**2).sum(axis=1))\n        comp_length = np.max(np.sqrt(((all_coords - all_coords.mean(axis=0))**2).sum(axis=1)))\n        dist_to_closest_end = dists_to_all.min()  # should be ~0 since closest_pt is on comp\n\n        # find actual endpoints (most distant from centroid)\n        centroid = all_coords.mean(axis=0)\n        dist_from_centroid = np.sqrt(((all_coords - centroid)**2).sum(axis=1))\n        # top 2 most distant = the two tips\n        tip_indices = np.argsort(dist_from_centroid)[-2:]\n        tips = all_coords[tip_indices]\n\n        # is closest_pt near either tip?\n        dist_to_tips = np.sqrt(((tips - closest_pt)**2).sum(axis=1))\n        return dist_to_tips.min() &lt; fraction * comp_length\n\n    tip1 = near_tip(closest1, coords1, tip_fraction)\n    tip2 = near_tip(closest2, coords2, tip_fraction)\n\n    # hole = BOTH closest approaches are near tips\n    return tip1 and tip2\n\n\ndef cut_touching_sheets_tracking(\n    mask,\n    max_shift=8,\n    min_component_size=10,\n    cut_radius=1,\n    tip_fraction=0.2,    # what fraction of line length counts as tip region\n):\n    result = mask.copy().astype(np.uint8)\n    D = mask.shape[0]\n\n    # precompute skeletons\n    skels        = []\n    comp_labeled = []\n    comp_n       = []\n\n    for z in range(D):\n        slc = mask[z].astype(bool)\n        if not slc.any():\n            skels.append(None)\n            comp_labeled.append(None)\n            comp_n.append(0)\n            continue\n\n        skel = skeletonize(slc)\n        labeled, n = ndi_label(skel)\n        for i in range(1, n + 1):\n            if (labeled == i).sum() &lt; min_component_size:\n                skel[labeled == i] = False\n        labeled, n = ndi_label(skel)\n\n        skels.append(skel)\n        comp_labeled.append(labeled)\n        comp_n.append(n)\n\n    shift_struct = np.ones((2*max_shift+1, 2*max_shift+1), dtype=bool)\n    cut_struct   = np.ones((2*cut_radius+1, 2*cut_radius+1), dtype=bool)\n\n    for z in range(1, D):\n        if skels[z] is None or skels[z-1] is None:\n            continue\n        if comp_n[z] == 0 or comp_n[z-1] == 0:\n            continue\n\n        prev_labeled = comp_labeled[z-1]\n        curr_labeled = comp_labeled[z]\n        n_curr       = comp_n[z]\n\n        prev_dilated = binary_dilation(skels[z-1], structure=shift_struct)\n\n        for i in range(1, n_curr + 1):\n            curr_comp = (curr_labeled == i)\n            overlap   = curr_comp &amp; prev_dilated\n            prev_ids  = np.unique(prev_labeled[overlap])\n            prev_ids  = prev_ids[prev_ids &gt; 0]\n\n            if len(prev_ids) &lt; 2:\n                continue\n\n            # merge event — check if it's tip-to-tip (hole) or side-to-side (touching sheets)\n            comp_a = (prev_labeled == prev_ids[0])\n            comp_b = (prev_labeled == prev_ids[1])\n\n            if is_tip_to_tip(comp_a, comp_b, tip_fraction):\n                # end-to-end = hole in single sheet, skip\n                continue\n\n            # side-to-side = two separate sheets touching → cut\n            curr_skel = skels[z] &amp; (curr_labeled == i)\n            neighbor_count = convolve(\n                curr_skel.astype(np.uint8),\n                np.ones((3,3), dtype=np.uint8),\n                mode='constant'\n            )\n            branch_points = curr_skel &amp; (neighbor_count &gt;= 4)\n\n            if not branch_points.any():\n                ys, xs = np.where(curr_skel)\n                bp = np.zeros_like(curr_skel, dtype=bool)\n                bp[int(ys.mean()), int(xs.mean())] = True\n                branch_points = bp\n\n            cut_region = binary_dilation(branch_points, structure=cut_struct)\n            result[z] = result[z].astype(bool) &amp; ~cut_region\n\n    return result.astype(np.uint8)\n</code></pre>\n<p>the postprocessing technique that we used was the same topo postprocess function used in the public notebooks, but i moved t_high parameter from 0.9 to 0.75 as these seems to do fragmentation which exploit bug in the metric that was discussed in the last days of the competition and it actually improved the performance a bit, and as mentioned in the discussions it seems that if the ground truth has no holes it would be better to add a single hole rather than none, I tried to exploit this in the last day such that if a 3d component has no holes at all I add a single  hole, didn't show improvement probably because the ignore mask was already introducing some holes?</p>\n<p>Anyways that was our solution and hopefully it was insightful and clear enough</p>",
  "messages": [
    {
      "id": 3414966,
      "postDate": "2026-02-28T02:23:13.580Z",
      "content": "<p>thanks to kaggle and the host team for this great competition</p>\n<p>this is conclusion of our solution </p>\n<h1>Modeling and Ensembling</h1>\n<p>In the final days we used multiple combinations of these models( patch size = 160 for all of them)</p>\n<p>1)W-net (included in nnUnet framework better performance than nnUnet but slow in inference time which made the ensembling harder)</p>\n<p>2)Ukan( Unet based on kolmogorov arnold networks it performs well and very fast slightly lower performance than nnUnet but good for diversity)</p>\n<p>3)nnUNet default</p>\n<p>4)nnUNet MedialSurfaceRecall </p>\n<p>5) nnUNet CLDiceLoss</p>\n<p>6) nnUNet MedialTverskySurfaceRecall</p>\n<p>we tried many combinations for the ensemble and looking at the best notebook we have submitted it seems that it included 4 models\nworth noting that we tried to use different validation fold for each model to increase diversity and to not make all models ignore the fold 0 val data</p>\n<p>for the  ensemble logic we used adaptive merging made by <a href=\"https://www.kaggle.com/i2nfinit3y\" target=\"_blank\">@i2nfinit3y</a> </p>\n<pre><code>def adaptive_merge_probs(probs_list, window_size=9, detail_scale=1.0, smoothing=1.0):\n    preds = [p.astype(np.float32) for p in probs_list]\n\n    # 1. (Local Mean)\n    # uniform_filter 是极其高效的盒状滤波\n    local_means = [uniform_filter(pred, size=window_size) for pred in preds]\n\n    # 2. (Local Variance)\n    # Var = Mean((X - Mean)^2)\n    variances = [\n        uniform_filter((pred - mean) ** 2, size=window_size) * detail_scale\n        for pred, mean in zip(preds, local_means)\n    ]\n\n    # 3. (Smoothing)\n    if smoothing &gt; 0:\n        variances = [uniform_filter(var, size=int(window_size * smoothing)) for var in variances]\n\n    # 4. (Weights)\n    weights = [var / (var.max() + 1e-6) for var in variances]\n\n    weight_sum = sum(weights)\n    weight_sum = weight_sum + 1e-8\n\n    # 5. (Weighted Merge)\n    merged = sum(pred * weight for pred, weight in zip(preds, weights)) / weight_sum\n\n    return merged\n</code></pre>\n<p>also to ensure that the ensemble works fine  <a href=\"https://www.kaggle.com/i2nfinit3y\" target=\"_blank\">@i2nfinit3y</a>  made the  parrallization   code for the 2 T4s</p>\n<p>we also tried many approaches like 3Dino and  convnext 2.5d models,I have tried to use MedDinov3 which achieve SOTA in 2d medical segmentation where i introduced each 3 slices as RGB channels inspired by <a href=\"https://www.kaggle.com/tom99763\" target=\"_blank\">@tom99763</a> solution in the <a href=\"https://www.kaggle.com/competitions/rsna-intracranial-aneurysm-detection/writeups/9th-place-solution\" target=\"_blank\">RNSA</a> competition but it was bad and no where close to nnUnet</p>\n<h1>Postprocessing</h1>\n<p>this seemed to be the most important part yet the most counterproductive part, we tried many things like that were supposed to increase the lb but the opposite happened including holes closing, also I had an idea in the last 3 days were I though  about an algorithm that looks at the volume from top where it sees the upper edges of the papers and then go down from slice to slice, if 2 lines/components that are slightly parallel combined into a single one then it is  a wrong merge and it add a small cut, they should be parallel and not close to each other tips or else that is just the end of a hole/tunnel in a sheet that merge again, unfortunately I didn't have enough time to try and refine it specially that we had other experiments to try, but I thought that it was worth mentioning this is a  simple clauded sample  of the code which probably have problems and buggy</p>\n<pre><code>from skimage.morphology import skeletonize\nfrom scipy.ndimage import label as ndi_label, binary_dilation, convolve\nimport numpy as np\n\ndef get_endpoints(skel_comp):\n    \"\"\"Find tip pixels of a skeleton component — pixels with exactly 1 neighbor.\"\"\"\n    neighbor_count = convolve(\n        skel_comp.astype(np.uint8),\n        np.ones((3,3), dtype=np.uint8),\n        mode='constant'\n    )\n    # endpoint = skeleton pixel with exactly 2 neighbors (itself + 1 other)\n    endpoints = skel_comp &amp; (neighbor_count == 2)\n    return np.argwhere(endpoints)\n\n\ndef is_tip_to_tip(comp1, comp2, tip_fraction=0.2):\n    \"\"\"\n    Returns True if the closest approach between comp1 and comp2\n    is near the TIPS of both — meaning they are end-to-end (hole in single sheet).\n    Returns False if the closest approach is near their bodies — side-to-side (touching sheets).\n\n    tip_fraction: what fraction of the component length counts as \"tip region\"\n    \"\"\"\n    coords1 = np.argwhere(comp1)\n    coords2 = np.argwhere(comp2)\n\n    if len(coords1) == 0 or len(coords2) == 0:\n        return False\n\n    # Find the closest pair of points between the two components\n    # For efficiency: sample if too large\n    MAX_SAMPLE = 200\n    s1 = coords1[np.random.choice(len(coords1), min(MAX_SAMPLE, len(coords1)), replace=False)]\n    s2 = coords2[np.random.choice(len(coords2), min(MAX_SAMPLE, len(coords2)), replace=False)]\n\n    # Pairwise distances\n    diff = s1[:, None, :] - s2[None, :, :]   # (N1, N2, 2)\n    dists = np.sqrt((diff**2).sum(axis=2))    # (N1, N2)\n    min_idx = np.unravel_index(np.argmin(dists), dists.shape)\n    closest1 = s1[min_idx[0]]  # closest point on comp1\n    closest2 = s2[min_idx[1]]  # closest point on comp2\n\n    # Check if closest point on each component is near its tip\n    # by measuring how far it is from the component's endpoints\n    def near_tip(closest_pt, all_coords, fraction):\n        \"\"\"True if closest_pt is within fraction*length of either endpoint.\"\"\"\n        # endpoints = most extreme points along the principal axis\n        # simple proxy: just check distance to the two most distant points\n        dists_to_all = np.sqrt(((all_coords - closest_pt)**2).sum(axis=1))\n        comp_length = np.max(np.sqrt(((all_coords - all_coords.mean(axis=0))**2).sum(axis=1)))\n        dist_to_closest_end = dists_to_all.min()  # should be ~0 since closest_pt is on comp\n\n        # find actual endpoints (most distant from centroid)\n        centroid = all_coords.mean(axis=0)\n        dist_from_centroid = np.sqrt(((all_coords - centroid)**2).sum(axis=1))\n        # top 2 most distant = the two tips\n        tip_indices = np.argsort(dist_from_centroid)[-2:]\n        tips = all_coords[tip_indices]\n\n        # is closest_pt near either tip?\n        dist_to_tips = np.sqrt(((tips - closest_pt)**2).sum(axis=1))\n        return dist_to_tips.min() &lt; fraction * comp_length\n\n    tip1 = near_tip(closest1, coords1, tip_fraction)\n    tip2 = near_tip(closest2, coords2, tip_fraction)\n\n    # hole = BOTH closest approaches are near tips\n    return tip1 and tip2\n\n\ndef cut_touching_sheets_tracking(\n    mask,\n    max_shift=8,\n    min_component_size=10,\n    cut_radius=1,\n    tip_fraction=0.2,    # what fraction of line length counts as tip region\n):\n    result = mask.copy().astype(np.uint8)\n    D = mask.shape[0]\n\n    # precompute skeletons\n    skels        = []\n    comp_labeled = []\n    comp_n       = []\n\n    for z in range(D):\n        slc = mask[z].astype(bool)\n        if not slc.any():\n            skels.append(None)\n            comp_labeled.append(None)\n            comp_n.append(0)\n            continue\n\n        skel = skeletonize(slc)\n        labeled, n = ndi_label(skel)\n        for i in range(1, n + 1):\n            if (labeled == i).sum() &lt; min_component_size:\n                skel[labeled == i] = False\n        labeled, n = ndi_label(skel)\n\n        skels.append(skel)\n        comp_labeled.append(labeled)\n        comp_n.append(n)\n\n    shift_struct = np.ones((2*max_shift+1, 2*max_shift+1), dtype=bool)\n    cut_struct   = np.ones((2*cut_radius+1, 2*cut_radius+1), dtype=bool)\n\n    for z in range(1, D):\n        if skels[z] is None or skels[z-1] is None:\n            continue\n        if comp_n[z] == 0 or comp_n[z-1] == 0:\n            continue\n\n        prev_labeled = comp_labeled[z-1]\n        curr_labeled = comp_labeled[z]\n        n_curr       = comp_n[z]\n\n        prev_dilated = binary_dilation(skels[z-1], structure=shift_struct)\n\n        for i in range(1, n_curr + 1):\n            curr_comp = (curr_labeled == i)\n            overlap   = curr_comp &amp; prev_dilated\n            prev_ids  = np.unique(prev_labeled[overlap])\n            prev_ids  = prev_ids[prev_ids &gt; 0]\n\n            if len(prev_ids) &lt; 2:\n                continue\n\n            # merge event — check if it's tip-to-tip (hole) or side-to-side (touching sheets)\n            comp_a = (prev_labeled == prev_ids[0])\n            comp_b = (prev_labeled == prev_ids[1])\n\n            if is_tip_to_tip(comp_a, comp_b, tip_fraction):\n                # end-to-end = hole in single sheet, skip\n                continue\n\n            # side-to-side = two separate sheets touching → cut\n            curr_skel = skels[z] &amp; (curr_labeled == i)\n            neighbor_count = convolve(\n                curr_skel.astype(np.uint8),\n                np.ones((3,3), dtype=np.uint8),\n                mode='constant'\n            )\n            branch_points = curr_skel &amp; (neighbor_count &gt;= 4)\n\n            if not branch_points.any():\n                ys, xs = np.where(curr_skel)\n                bp = np.zeros_like(curr_skel, dtype=bool)\n                bp[int(ys.mean()), int(xs.mean())] = True\n                branch_points = bp\n\n            cut_region = binary_dilation(branch_points, structure=cut_struct)\n            result[z] = result[z].astype(bool) &amp; ~cut_region\n\n    return result.astype(np.uint8)\n</code></pre>\n<p>the postprocessing technique that we used was the same topo postprocess function used in the public notebooks, but i moved t_high parameter from 0.9 to 0.75 as these seems to do fragmentation which exploit bug in the metric that was discussed in the last days of the competition and it actually improved the performance a bit, and as mentioned in the discussions it seems that if the ground truth has no holes it would be better to add a single hole rather than none, I tried to exploit this in the last day such that if a 3d component has no holes at all I add a single  hole, didn't show improvement probably because the ignore mask was already introducing some holes?</p>\n<p>Anyways that was our solution and hopefully it was insightful and clear enough</p>",
      "rawMarkdown": "thanks to kaggle and the host team for this great competition\n\nthis is conclusion of our solution \n\n\n# Modeling and Ensembling\n\nIn the final days we used multiple combinations of these models( patch size = 160 for all of them)\n\n1)W-net (included in nnUnet framework better performance than nnUnet but slow in inference time which made the ensembling harder)\n\n2)Ukan( Unet based on kolmogorov arnold networks it performs well and very fast slightly lower performance than nnUnet but good for diversity)\n\n3)nnUNet default\n\n4)nnUNet MedialSurfaceRecall \n\n5) nnUNet CLDiceLoss\n\n6) nnUNet MedialTverskySurfaceRecall\n\n\nwe tried many combinations for the ensemble and looking at the best notebook we have submitted it seems that it included 4 models\nworth noting that we tried to use different validation fold for each model to increase diversity and to not make all models ignore the fold 0 val data\n\nfor the  ensemble logic we used adaptive merging made by @i2nfinit3y \n\n```python\n\ndef adaptive_merge_probs(probs_list, window_size=9, detail_scale=1.0, smoothing=1.0):\n    preds = [p.astype(np.float32) for p in probs_list]\n    \n    # 1. (Local Mean)\n    # uniform_filter 是极其高效的盒状滤波\n    local_means = [uniform_filter(pred, size=window_size) for pred in preds]\n\n    # 2. (Local Variance)\n    # Var = Mean((X - Mean)^2)\n    variances = [\n        uniform_filter((pred - mean) ** 2, size=window_size) * detail_scale\n        for pred, mean in zip(preds, local_means)\n    ]\n\n    # 3. (Smoothing)\n    if smoothing > 0:\n        variances = [uniform_filter(var, size=int(window_size * smoothing)) for var in variances]\n\n    # 4. (Weights)\n    weights = [var / (var.max() + 1e-6) for var in variances]\n\n    weight_sum = sum(weights)\n    weight_sum = weight_sum + 1e-8\n\n    # 5. (Weighted Merge)\n    merged = sum(pred * weight for pred, weight in zip(preds, weights)) / weight_sum\n\n    return merged\n```\n\nalso to ensure that the ensemble works fine  @i2nfinit3y  made the  parrallization   code for the 2 T4s\n\n\n\nwe also tried many approaches like 3Dino and  convnext 2.5d models,I have tried to use MedDinov3 which achieve SOTA in 2d medical segmentation where i introduced each 3 slices as RGB channels inspired by @tom99763 solution in the [RNSA](https://www.kaggle.com/competitions/rsna-intracranial-aneurysm-detection/writeups/9th-place-solution) competition but it was bad and no where close to nnUnet\n\n\n# Postprocessing\n\nthis seemed to be the most important part yet the most counterproductive part, we tried many things like that were supposed to increase the lb but the opposite happened including holes closing, also I had an idea in the last 3 days were I though  about an algorithm that looks at the volume from top where it sees the upper edges of the papers and then go down from slice to slice, if 2 lines/components that are slightly parallel combined into a single one then it is  a wrong merge and it add a small cut, they should be parallel and not close to each other tips or else that is just the end of a hole/tunnel in a sheet that merge again, unfortunately I didn't have enough time to try and refine it specially that we had other experiments to try, but I thought that it was worth mentioning this is a  simple clauded sample  of the code which probably have problems and buggy\n```python\n\nfrom skimage.morphology import skeletonize\nfrom scipy.ndimage import label as ndi_label, binary_dilation, convolve\nimport numpy as np\n\ndef get_endpoints(skel_comp):\n    \"\"\"Find tip pixels of a skeleton component — pixels with exactly 1 neighbor.\"\"\"\n    neighbor_count = convolve(\n        skel_comp.astype(np.uint8),\n        np.ones((3,3), dtype=np.uint8),\n        mode='constant'\n    )\n    # endpoint = skeleton pixel with exactly 2 neighbors (itself + 1 other)\n    endpoints = skel_comp & (neighbor_count == 2)\n    return np.argwhere(endpoints)\n\n\ndef is_tip_to_tip(comp1, comp2, tip_fraction=0.2):\n    \"\"\"\n    Returns True if the closest approach between comp1 and comp2\n    is near the TIPS of both — meaning they are end-to-end (hole in single sheet).\n    Returns False if the closest approach is near their bodies — side-to-side (touching sheets).\n\n    tip_fraction: what fraction of the component length counts as \"tip region\"\n    \"\"\"\n    coords1 = np.argwhere(comp1)\n    coords2 = np.argwhere(comp2)\n\n    if len(coords1) == 0 or len(coords2) == 0:\n        return False\n\n    # Find the closest pair of points between the two components\n    # For efficiency: sample if too large\n    MAX_SAMPLE = 200\n    s1 = coords1[np.random.choice(len(coords1), min(MAX_SAMPLE, len(coords1)), replace=False)]\n    s2 = coords2[np.random.choice(len(coords2), min(MAX_SAMPLE, len(coords2)), replace=False)]\n\n    # Pairwise distances\n    diff = s1[:, None, :] - s2[None, :, :]   # (N1, N2, 2)\n    dists = np.sqrt((diff**2).sum(axis=2))    # (N1, N2)\n    min_idx = np.unravel_index(np.argmin(dists), dists.shape)\n    closest1 = s1[min_idx[0]]  # closest point on comp1\n    closest2 = s2[min_idx[1]]  # closest point on comp2\n\n    # Check if closest point on each component is near its tip\n    # by measuring how far it is from the component's endpoints\n    def near_tip(closest_pt, all_coords, fraction):\n        \"\"\"True if closest_pt is within fraction*length of either endpoint.\"\"\"\n        # endpoints = most extreme points along the principal axis\n        # simple proxy: just check distance to the two most distant points\n        dists_to_all = np.sqrt(((all_coords - closest_pt)**2).sum(axis=1))\n        comp_length = np.max(np.sqrt(((all_coords - all_coords.mean(axis=0))**2).sum(axis=1)))\n        dist_to_closest_end = dists_to_all.min()  # should be ~0 since closest_pt is on comp\n        \n        # find actual endpoints (most distant from centroid)\n        centroid = all_coords.mean(axis=0)\n        dist_from_centroid = np.sqrt(((all_coords - centroid)**2).sum(axis=1))\n        # top 2 most distant = the two tips\n        tip_indices = np.argsort(dist_from_centroid)[-2:]\n        tips = all_coords[tip_indices]\n        \n        # is closest_pt near either tip?\n        dist_to_tips = np.sqrt(((tips - closest_pt)**2).sum(axis=1))\n        return dist_to_tips.min() < fraction * comp_length\n\n    tip1 = near_tip(closest1, coords1, tip_fraction)\n    tip2 = near_tip(closest2, coords2, tip_fraction)\n\n    # hole = BOTH closest approaches are near tips\n    return tip1 and tip2\n\n\ndef cut_touching_sheets_tracking(\n    mask,\n    max_shift=8,\n    min_component_size=10,\n    cut_radius=1,\n    tip_fraction=0.2,    # what fraction of line length counts as tip region\n):\n    result = mask.copy().astype(np.uint8)\n    D = mask.shape[0]\n\n    # precompute skeletons\n    skels        = []\n    comp_labeled = []\n    comp_n       = []\n\n    for z in range(D):\n        slc = mask[z].astype(bool)\n        if not slc.any():\n            skels.append(None)\n            comp_labeled.append(None)\n            comp_n.append(0)\n            continue\n\n        skel = skeletonize(slc)\n        labeled, n = ndi_label(skel)\n        for i in range(1, n + 1):\n            if (labeled == i).sum() < min_component_size:\n                skel[labeled == i] = False\n        labeled, n = ndi_label(skel)\n\n        skels.append(skel)\n        comp_labeled.append(labeled)\n        comp_n.append(n)\n\n    shift_struct = np.ones((2*max_shift+1, 2*max_shift+1), dtype=bool)\n    cut_struct   = np.ones((2*cut_radius+1, 2*cut_radius+1), dtype=bool)\n\n    for z in range(1, D):\n        if skels[z] is None or skels[z-1] is None:\n            continue\n        if comp_n[z] == 0 or comp_n[z-1] == 0:\n            continue\n\n        prev_labeled = comp_labeled[z-1]\n        curr_labeled = comp_labeled[z]\n        n_curr       = comp_n[z]\n\n        prev_dilated = binary_dilation(skels[z-1], structure=shift_struct)\n\n        for i in range(1, n_curr + 1):\n            curr_comp = (curr_labeled == i)\n            overlap   = curr_comp & prev_dilated\n            prev_ids  = np.unique(prev_labeled[overlap])\n            prev_ids  = prev_ids[prev_ids > 0]\n\n            if len(prev_ids) < 2:\n                continue\n\n            # merge event — check if it's tip-to-tip (hole) or side-to-side (touching sheets)\n            comp_a = (prev_labeled == prev_ids[0])\n            comp_b = (prev_labeled == prev_ids[1])\n\n            if is_tip_to_tip(comp_a, comp_b, tip_fraction):\n                # end-to-end = hole in single sheet, skip\n                continue\n\n            # side-to-side = two separate sheets touching → cut\n            curr_skel = skels[z] & (curr_labeled == i)\n            neighbor_count = convolve(\n                curr_skel.astype(np.uint8),\n                np.ones((3,3), dtype=np.uint8),\n                mode='constant'\n            )\n            branch_points = curr_skel & (neighbor_count >= 4)\n\n            if not branch_points.any():\n                ys, xs = np.where(curr_skel)\n                bp = np.zeros_like(curr_skel, dtype=bool)\n                bp[int(ys.mean()), int(xs.mean())] = True\n                branch_points = bp\n\n            cut_region = binary_dilation(branch_points, structure=cut_struct)\n            result[z] = result[z].astype(bool) & ~cut_region\n\n    return result.astype(np.uint8)\n\n\n```\n\n\nthe postprocessing technique that we used was the same topo postprocess function used in the public notebooks, but i moved t_high parameter from 0.9 to 0.75 as these seems to do fragmentation which exploit bug in the metric that was discussed in the last days of the competition and it actually improved the performance a bit, and as mentioned in the discussions it seems that if the ground truth has no holes it would be better to add a single hole rather than none, I tried to exploit this in the last day such that if a 3d component has no holes at all I add a single  hole, didn't show improvement probably because the ignore mask was already introducing some holes?\n\n\n\nAnyways that was our solution and hopefully it was insightful and clear enough",
      "votes": 14
    }
  ],
  "comments": [],
  "raw_markdown_by_id": {
    "3414966": "thanks to kaggle and the host team for this great competition\n\nthis is conclusion of our solution \n\n\n# Modeling and Ensembling\n\nIn the final days we used multiple combinations of these models( patch size = 160 for all of them)\n\n1)W-net (included in nnUnet framework better performance than nnUnet but slow in inference time which made the ensembling harder)\n\n2)Ukan( Unet based on kolmogorov arnold networks it performs well and very fast slightly lower performance than nnUnet but good for diversity)\n\n3)nnUNet default\n\n4)nnUNet MedialSurfaceRecall \n\n5) nnUNet CLDiceLoss\n\n6) nnUNet MedialTverskySurfaceRecall\n\n\nwe tried many combinations for the ensemble and looking at the best notebook we have submitted it seems that it included 4 models\nworth noting that we tried to use different validation fold for each model to increase diversity and to not make all models ignore the fold 0 val data\n\nfor the  ensemble logic we used adaptive merging made by @i2nfinit3y \n\n```python\n\ndef adaptive_merge_probs(probs_list, window_size=9, detail_scale=1.0, smoothing=1.0):\n    preds = [p.astype(np.float32) for p in probs_list]\n    \n    # 1. (Local Mean)\n    # uniform_filter 是极其高效的盒状滤波\n    local_means = [uniform_filter(pred, size=window_size) for pred in preds]\n\n    # 2. (Local Variance)\n    # Var = Mean((X - Mean)^2)\n    variances = [\n        uniform_filter((pred - mean) ** 2, size=window_size) * detail_scale\n        for pred, mean in zip(preds, local_means)\n    ]\n\n    # 3. (Smoothing)\n    if smoothing > 0:\n        variances = [uniform_filter(var, size=int(window_size * smoothing)) for var in variances]\n\n    # 4. (Weights)\n    weights = [var / (var.max() + 1e-6) for var in variances]\n\n    weight_sum = sum(weights)\n    weight_sum = weight_sum + 1e-8\n\n    # 5. (Weighted Merge)\n    merged = sum(pred * weight for pred, weight in zip(preds, weights)) / weight_sum\n\n    return merged\n```\n\nalso to ensure that the ensemble works fine  @i2nfinit3y  made the  parrallization   code for the 2 T4s\n\n\n\nwe also tried many approaches like 3Dino and  convnext 2.5d models,I have tried to use MedDinov3 which achieve SOTA in 2d medical segmentation where i introduced each 3 slices as RGB channels inspired by @tom99763 solution in the [RNSA](https://www.kaggle.com/competitions/rsna-intracranial-aneurysm-detection/writeups/9th-place-solution) competition but it was bad and no where close to nnUnet\n\n\n# Postprocessing\n\nthis seemed to be the most important part yet the most counterproductive part, we tried many things like that were supposed to increase the lb but the opposite happened including holes closing, also I had an idea in the last 3 days were I though  about an algorithm that looks at the volume from top where it sees the upper edges of the papers and then go down from slice to slice, if 2 lines/components that are slightly parallel combined into a single one then it is  a wrong merge and it add a small cut, they should be parallel and not close to each other tips or else that is just the end of a hole/tunnel in a sheet that merge again, unfortunately I didn't have enough time to try and refine it specially that we had other experiments to try, but I thought that it was worth mentioning this is a  simple clauded sample  of the code which probably have problems and buggy\n```python\n\nfrom skimage.morphology import skeletonize\nfrom scipy.ndimage import label as ndi_label, binary_dilation, convolve\nimport numpy as np\n\ndef get_endpoints(skel_comp):\n    \"\"\"Find tip pixels of a skeleton component — pixels with exactly 1 neighbor.\"\"\"\n    neighbor_count = convolve(\n        skel_comp.astype(np.uint8),\n        np.ones((3,3), dtype=np.uint8),\n        mode='constant'\n    )\n    # endpoint = skeleton pixel with exactly 2 neighbors (itself + 1 other)\n    endpoints = skel_comp & (neighbor_count == 2)\n    return np.argwhere(endpoints)\n\n\ndef is_tip_to_tip(comp1, comp2, tip_fraction=0.2):\n    \"\"\"\n    Returns True if the closest approach between comp1 and comp2\n    is near the TIPS of both — meaning they are end-to-end (hole in single sheet).\n    Returns False if the closest approach is near their bodies — side-to-side (touching sheets).\n\n    tip_fraction: what fraction of the component length counts as \"tip region\"\n    \"\"\"\n    coords1 = np.argwhere(comp1)\n    coords2 = np.argwhere(comp2)\n\n    if len(coords1) == 0 or len(coords2) == 0:\n        return False\n\n    # Find the closest pair of points between the two components\n    # For efficiency: sample if too large\n    MAX_SAMPLE = 200\n    s1 = coords1[np.random.choice(len(coords1), min(MAX_SAMPLE, len(coords1)), replace=False)]\n    s2 = coords2[np.random.choice(len(coords2), min(MAX_SAMPLE, len(coords2)), replace=False)]\n\n    # Pairwise distances\n    diff = s1[:, None, :] - s2[None, :, :]   # (N1, N2, 2)\n    dists = np.sqrt((diff**2).sum(axis=2))    # (N1, N2)\n    min_idx = np.unravel_index(np.argmin(dists), dists.shape)\n    closest1 = s1[min_idx[0]]  # closest point on comp1\n    closest2 = s2[min_idx[1]]  # closest point on comp2\n\n    # Check if closest point on each component is near its tip\n    # by measuring how far it is from the component's endpoints\n    def near_tip(closest_pt, all_coords, fraction):\n        \"\"\"True if closest_pt is within fraction*length of either endpoint.\"\"\"\n        # endpoints = most extreme points along the principal axis\n        # simple proxy: just check distance to the two most distant points\n        dists_to_all = np.sqrt(((all_coords - closest_pt)**2).sum(axis=1))\n        comp_length = np.max(np.sqrt(((all_coords - all_coords.mean(axis=0))**2).sum(axis=1)))\n        dist_to_closest_end = dists_to_all.min()  # should be ~0 since closest_pt is on comp\n        \n        # find actual endpoints (most distant from centroid)\n        centroid = all_coords.mean(axis=0)\n        dist_from_centroid = np.sqrt(((all_coords - centroid)**2).sum(axis=1))\n        # top 2 most distant = the two tips\n        tip_indices = np.argsort(dist_from_centroid)[-2:]\n        tips = all_coords[tip_indices]\n        \n        # is closest_pt near either tip?\n        dist_to_tips = np.sqrt(((tips - closest_pt)**2).sum(axis=1))\n        return dist_to_tips.min() < fraction * comp_length\n\n    tip1 = near_tip(closest1, coords1, tip_fraction)\n    tip2 = near_tip(closest2, coords2, tip_fraction)\n\n    # hole = BOTH closest approaches are near tips\n    return tip1 and tip2\n\n\ndef cut_touching_sheets_tracking(\n    mask,\n    max_shift=8,\n    min_component_size=10,\n    cut_radius=1,\n    tip_fraction=0.2,    # what fraction of line length counts as tip region\n):\n    result = mask.copy().astype(np.uint8)\n    D = mask.shape[0]\n\n    # precompute skeletons\n    skels        = []\n    comp_labeled = []\n    comp_n       = []\n\n    for z in range(D):\n        slc = mask[z].astype(bool)\n        if not slc.any():\n            skels.append(None)\n            comp_labeled.append(None)\n            comp_n.append(0)\n            continue\n\n        skel = skeletonize(slc)\n        labeled, n = ndi_label(skel)\n        for i in range(1, n + 1):\n            if (labeled == i).sum() < min_component_size:\n                skel[labeled == i] = False\n        labeled, n = ndi_label(skel)\n\n        skels.append(skel)\n        comp_labeled.append(labeled)\n        comp_n.append(n)\n\n    shift_struct = np.ones((2*max_shift+1, 2*max_shift+1), dtype=bool)\n    cut_struct   = np.ones((2*cut_radius+1, 2*cut_radius+1), dtype=bool)\n\n    for z in range(1, D):\n        if skels[z] is None or skels[z-1] is None:\n            continue\n        if comp_n[z] == 0 or comp_n[z-1] == 0:\n            continue\n\n        prev_labeled = comp_labeled[z-1]\n        curr_labeled = comp_labeled[z]\n        n_curr       = comp_n[z]\n\n        prev_dilated = binary_dilation(skels[z-1], structure=shift_struct)\n\n        for i in range(1, n_curr + 1):\n            curr_comp = (curr_labeled == i)\n            overlap   = curr_comp & prev_dilated\n            prev_ids  = np.unique(prev_labeled[overlap])\n            prev_ids  = prev_ids[prev_ids > 0]\n\n            if len(prev_ids) < 2:\n                continue\n\n            # merge event — check if it's tip-to-tip (hole) or side-to-side (touching sheets)\n            comp_a = (prev_labeled == prev_ids[0])\n            comp_b = (prev_labeled == prev_ids[1])\n\n            if is_tip_to_tip(comp_a, comp_b, tip_fraction):\n                # end-to-end = hole in single sheet, skip\n                continue\n\n            # side-to-side = two separate sheets touching → cut\n            curr_skel = skels[z] & (curr_labeled == i)\n            neighbor_count = convolve(\n                curr_skel.astype(np.uint8),\n                np.ones((3,3), dtype=np.uint8),\n                mode='constant'\n            )\n            branch_points = curr_skel & (neighbor_count >= 4)\n\n            if not branch_points.any():\n                ys, xs = np.where(curr_skel)\n                bp = np.zeros_like(curr_skel, dtype=bool)\n                bp[int(ys.mean()), int(xs.mean())] = True\n                branch_points = bp\n\n            cut_region = binary_dilation(branch_points, structure=cut_struct)\n            result[z] = result[z].astype(bool) & ~cut_region\n\n    return result.astype(np.uint8)\n\n\n```\n\n\nthe postprocessing technique that we used was the same topo postprocess function used in the public notebooks, but i moved t_high parameter from 0.9 to 0.75 as these seems to do fragmentation which exploit bug in the metric that was discussed in the last days of the competition and it actually improved the performance a bit, and as mentioned in the discussions it seems that if the ground truth has no holes it would be better to add a single hole rather than none, I tried to exploit this in the last day such that if a 3d component has no holes at all I add a single  hole, didn't show improvement probably because the ignore mask was already introducing some holes?\n\n\n\nAnyways that was our solution and hopefully it was insightful and clear enough"
  }
}