<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ MachineLearning - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ MachineLearning - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 18 Sep 2026 23:38:59 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/machinelearning/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Turn a RECIST Line into a 3D Tumor Segmentation Mask ]]>
                </title>
                <description>
                    <![CDATA[ A radiologist can mark a tumor on a CT scan by drawing a straight line across it. Creating a complete 3D segmentation requires outlining the tumor across the slices where it appears, which is more tim ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-turn-a-recist-line-into-a-3d-tumor-segmentation-mask/</link>
                <guid isPermaLink="false">6aadae08b3c208add9878388</guid>
                
                    <category>
                        <![CDATA[ MachineLearning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Deep Learning ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Artificial Intelligence ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Medical Imaging ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Computer Vision ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HealthcareAI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Lakshmi Mahabaleshwara ]]>
                </dc:creator>
                <pubDate>Fri, 18 Sep 2026 21:32:56 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/bde6732f-ad81-4a51-8939-ec74637d09e6.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A radiologist can mark a tumor on a CT scan by drawing a straight line across it. Creating a complete 3D segmentation requires outlining the tumor across the slices where it appears, which is more time-consuming.</p>
<p>This tutorial explains how Lumina works. It's a system that uses a CT scan and one RECIST line to produce a 3D segmentation mask of the marked tumor.</p>
<p>Lumina was developed for the <a href="https://www.codabench.org/competitions/17652/"><strong>FLARE 2026 pan-cancer segmentation challenge</strong></a>. The system was designed to run on a CPU with an 8 GB memory limit and a 60-second inference limit.</p>
<p>This tutorial covers the main design decisions, implementation details, and experiments that shaped the final system.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-what-lumina-does">What Lumina Does</a></p>
<ul>
<li><a href="#heading-lumina-pipeline-overview">Lumina Pipeline Overview</a></li>
</ul>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-step-1-review-your-data-before-coding">Step 1: Review Your Data Before Coding</a></p>
<ul>
<li><p><a href="#heading-the-scans-were-already-brightness-adjusted">The Scans Were Already Brightness-Adjusted</a></p>
</li>
<li><p><a href="#heading-coordinate-order-matters">Coordinate Order Matters</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-2-turn-the-recist-line-into-network-input">Step 2: Turn the RECIST Line into Network Input</a></p>
<ul>
<li><a href="#heading-draw-the-line-at-the-required-resolution">Draw the Line at the Required Resolution</a></li>
</ul>
</li>
<li><p><a href="#heading-step-3-align-all-tumors-on-a-common-grid">Step 3: Align All Tumors on a Common Grid</a></p>
<ul>
<li><a href="#heading-intensity-normalization">Intensity Normalization</a></li>
</ul>
</li>
<li><p><a href="#heading-step-4-build-the-3d-segmentation-network">Step 4: Build the 3D Segmentation Network</a></p>
</li>
<li><p><a href="#heading-step-5-use-a-loss-function-that-includes-boundary-information">Step 5: Use a Loss Function That Includes Boundary Information</a></p>
<ul>
<li><a href="#heading-comparing-boundary-losses">Comparing Boundary Losses</a></li>
</ul>
</li>
<li><p><a href="#heading-step-6-transform-the-probability-map-into-a-segmentation-mask">Step 6: Transform the Probability Map into a Segmentation Mask</a></p>
<ul>
<li><p><a href="#heading-resample-the-probability-map">Resample the Probability Map</a></p>
</li>
<li><p><a href="#heading-apply-the-threshold">Apply the Threshold</a></p>
</li>
<li><p><a href="#heading-keep-the-tumor-connected-to-the-recist-line">Keep the Tumor Connected to the RECIST Line</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-7-accelerate-and-ensure-consistency-in-cpu-inference">Step 7: Accelerate and Ensure Consistency in CPU Inference</a></p>
<ul>
<li><p><a href="#heading-pin-the-thread-count">Pin the Thread Count</a></p>
</li>
<li><p><a href="#heading-budget-the-inference-passes">Budget the Inference Passes</a></p>
</li>
<li><p><a href="#heading-handle-individual-failures">Handle Individual Failures</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-the-results">The Results</a></p>
<ul>
<li><a href="#heading-qualitative-results">Qualitative results</a></li>
</ul>
</li>
<li><p><a href="#heading-what-the-radiologist-review-showed">What the Radiologist Review Showed</a></p>
</li>
<li><p><a href="#heading-three-lessons-that-apply-to-other-projects">Three Lessons That Apply to Other Projects</a></p>
<ul>
<li><p><a href="#heading-1-make-your-validation-data-representative">1. Make Your Validation Data Representative</a></p>
</li>
<li><p><a href="#heading-2-measure-the-limits-of-your-preprocessing">2. Measure the Limits of Your Preprocessing</a></p>
</li>
<li><p><a href="#heading-3-record-where-your-numbers-come-from">3. Record Where Your Numbers Come From</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-lumina-does">What Lumina Does</h2>
<p>A radiologist can measure a tumor by drawing a line across its longest visible diameter. This is a <strong>RECIST measurement</strong> (Response Evaluation Criteria in Solid Tumors), which is widely used to measure tumor response during cancer treatment.</p>
<p>A RECIST line measures a 2D diameter marker. It doesn't describe the tumor's full 3D shape.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69fd77e89f93a850a46d376f/ac6b8049-9e84-4e68-803f-8f04f92adb04.png" alt="Axial CT image of the abdomen with a green diameter marker placed across a liver lesion." style="display: block;" width="600" height="400" loading="lazy">

<p>Lumina uses this measurement to prompt a 3D segmentation model.</p>
<p>The task can be described as:</p>
<blockquote>
<p><strong>Input:</strong> A 3D CT scan and a 2D RECIST line marking one tumor.</p>
<p><strong>Output:</strong> A 3D mask of that tumor, with one label for each voxel.</p>
</blockquote>
<p>A <strong>voxel</strong> is the 3D equivalent of a pixel. Each voxel represents a small volume of tissue in the CT scan.</p>
<p>The RECIST line tells the model which tumor to segment. The model then predicts the tumor's three-dimensional extent.</p>
<p>This makes the segmentation problem more specific because the model doesn't need to identify every possible tumor in the scan.</p>
<h3 id="heading-lumina-pipeline-overview">Lumina Pipeline Overview</h3>
<p>The complete process has several stages. We start with a 3D CT scan and a RECIST line marking one tumor. We then convert the line into additional input channels, crop and resample the image, and pass the three channels through a 3D segmentation network.</p>
<p>The network produces a probability map, which we convert into the final 3D tumor mask through resampling, thresholding, and connected-component selection.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69fd77e89f93a850a46d376f/dc5e85bd-a319-41e7-be93-0686bb8acea8.png" alt="Diagram showing the Lumina pipeline from a CT scan and RECIST line to a 3D tumor segmentation mask." style="display: block;" width="600" height="400" loading="lazy">

<p>The main stages are:</p>
<ol>
<li><p>Prepare the CT scan and RECIST marker.</p>
</li>
<li><p>Encode the RECIST line as additional network input.</p>
</li>
<li><p>Crop and resample the image to a common grid.</p>
</li>
<li><p>Predict the tumor probability map with a 3D segmentation network.</p>
</li>
<li><p>Refine the probability map and convert it into a binary mask.</p>
</li>
<li><p>Control CPU inference so the full case stays within runtime and memory limits.</p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You'll get more from this tutorial if you have some experience with:</p>
<ul>
<li><p>Python</p>
</li>
<li><p>NumPy</p>
</li>
<li><p>PyTorch</p>
</li>
<li><p>Basic convolutional neural networks.</p>
</li>
</ul>
<p>No detailed medical background is required. The tutorial explains medical imaging concepts as it introduces them.</p>
<p>The implementation uses NumPy, SciPy, PyTorch, and <a href="https://project-monai.github.io/">MONAI</a>, a medical imaging framework built on PyTorch.</p>
<h2 id="heading-step-1-review-your-data-before-coding">Step 1: Review Your Data Before Coding</h2>
<p>Our data was provided as <code>.npz</code> files, which is NumPy's compressed array format.</p>
<p>Each file contains fields similar to these:</p>
<pre><code class="language-python">imgs       # the CT scan, a 3D array
recist     # the marker lines, same shape, one integer per tumour
spacing    # how many millimetres apart the voxels are
origin     # where the scan sits in the scanner's coordinates
direction  # how the scan is rotated
gts        # the ground-truth segmentation, training files only
</code></pre>
<p>Before building the model, we inspected the image values, array shapes, and spatial metadata.</p>
<p>Two details were especially significant.</p>
<h3 id="heading-the-scans-were-already-brightness-adjusted">The Scans Were Already Brightness-Adjusted</h3>
<p>CT scanners normally store images in Hounsfield units. Water is about 0 HU, while bone can exceed 1000 HU.</p>
<p>In this dataset, the scans had already been converted to a fixed 0–255 range. The original Hounsfield-unit values weren't available.</p>
<p>This meant that we couldn't apply the usual CT windowing process to the original values. Instead, we measured the intensity distribution of the data provided.</p>
<p>For example, 53.4% of the voxels had a value of exactly 0, corresponding to air outside the body.</p>
<h3 id="heading-coordinate-order-matters">Coordinate Order Matters</h3>
<p>The <code>spacing</code> array is stored as <code>(X, Y, Z)</code>, while NumPy arrays are indexed as <code>(Z, Y, X)</code>.</p>
<p>If you mix these two conventions, spatial measurements can be incorrect.</p>
<p>We converted the coordinates once at the input boundary and used the <code>(Z, Y, X)</code> convention internally:</p>
<pre><code class="language-python">def _to_zyx(vec3, order):
    vec3 = np.asarray(vec3, dtype=float).ravel()
    if order == "xyz":
        return vec3[::-1].copy()      # (x, y, z) -&gt; (z, y, x)
    if order == "zyx":
        return vec3.copy()
    raise ValueError(f"unknown geometry_order {order!r}")
</code></pre>
<p>The important lesson is to inspect several files before designing the preprocessing pipeline.</p>
<p>Check:</p>
<ul>
<li><p>Array shapes</p>
</li>
<li><p>Intensity ranges</p>
</li>
<li><p>Voxel spacing</p>
</li>
<li><p>Coordinate conventions</p>
</li>
<li><p>Metadata</p>
</li>
<li><p>Available labels</p>
</li>
</ul>
<p>Avoid assuming the data follows conventions from another CT dataset or tutorial.</p>
<h2 id="heading-step-2-turn-the-recist-line-into-network-input">Step 2: Turn the RECIST Line into Network Input</h2>
<p>A neural network receives a stack of input channels. The CT scan provides the first channel. We then represent the RECIST line in a form the network can use.</p>
<p>We use three channels:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69fd77e89f93a850a46d376f/e67ad26c-a952-4e99-8f45-463667e5de7b.png" alt="CT image and the two RECIST prompt channels used as input to Lumina: the RECIST line and endpoint heatmap." style="display: block;" width="600" height="400" loading="lazy">

<ul>
<li><p>CT scan</p>
</li>
<li><p>RECIST line, drawn 3 voxels thick</p>
</li>
<li><p>Endpoint heatmap, containing a Gaussian around each endpoint</p>
</li>
</ul>
<p>The two endpoints define the measured diameter and provide the network with the location and length of the RECIST measurement.</p>
<p>The endpoint information is represented using Gaussian functions. Each Gaussian has a high value near the endpoint and gradually decreases with distance.</p>
<pre><code class="language-python">def _endpoint_heatmap(endpoints_zyx, shape, sigma):
    d, h, w = (int(s) for s in shape)
    heat = np.zeros((d, h, w), dtype=np.float32)

    rad = max(1, int(np.ceil(3 * sigma)))
    two_s2 = 2.0 * sigma * sigma

    for z0, y0, x0 in np.asarray(endpoints_zyx, float):
        zc, yc, xc = int(round(z0)), int(round(y0)), int(round(x0)))

        zl, zr = max(0, zc - rad), min(d, zc + rad + 1)
        yl, yr = max(0, yc - rad), min(h, yc + rad + 1)
        xl, xr = max(0, xc - rad), min(w, xc + rad + 1)

        zz, yy, xx = np.mgrid[
            zl:zr, yl:yr, xl:xr
        ].astype(np.float32)

        g = np.exp(
            -((zz-z0)**2 + (yy-y0)**2 + (xx-x0)**2) / two_s2
        )

        np.maximum(
            heat[zl:zr, yl:yr, xl:xr],
            g,
            out=heat[zl:zr, yl:yr, xl:xr]
        )

    return heat
</code></pre>
<p>The Gaussian is calculated only in a small region around each endpoint.</p>
<p>With <code>sigma = 1.5</code>, values become very small several voxels away from the endpoint. Calculating the Gaussian over the complete volume would therefore perform unnecessary work.</p>
<h3 id="heading-draw-the-line-at-the-required-resolution">Draw the Line at the Required Resolution</h3>
<p>We also redraw the RECIST line from its two endpoints at the resolution the network requires.</p>
<p>We don't create the line at one resolution and then resize it with the image. Resizing a thin line can make it thinner or cause it to disconnect. Recreating the line from its endpoints keeps the prompt consistent with the image resolution.</p>
<h2 id="heading-step-3-align-all-tumors-on-a-common-grid">Step 3: Align All Tumors on a Common Grid</h2>
<p>CT scans can have different voxel spacings.</p>
<p>For example, one scan may have thin slices while another may have much thicker slices. The physical size represented by the same number of voxels can therefore vary between scans.</p>
<p>For each marked tumor, we'll create a fixed-size crop and resample it to a fixed voxel spacing.</p>
<p>Our configuration is:</p>
<pre><code class="language-yaml">target_spacing: [2.5, 1.0, 1.0]     # mm per voxel: z, y, x
crop_size:      [64, 160, 160]      # voxels: z, y, x
</code></pre>
<p>The in-plane dimensions correspond to a physical field of view of:</p>
<pre><code class="language-plaintext">160 × 160 mm
</code></pre>
<p>The network therefore receives a consistent <code>64 × 160 × 160</code> input.</p>
<p>We use a target spacing derived from the training data. The 2.5 mm slice spacing was close to the median slice spacing in the training set.</p>
<p>We use a 160 mm crop based on the distribution of lesion sizes in the training data. It covers the 99th percentile of lesion extent.</p>
<h3 id="heading-intensity-normalization">Intensity Normalization</h3>
<p>We also normalize the CT intensity values using statistics calculated from the training crops:</p>
<pre><code class="language-yaml">intensity_mean: 96.88
intensity_std: 79.00
</code></pre>
<p>The normalization is:</p>
<pre><code class="language-python">image = (image - 96.88) / 79.00
</code></pre>
<p>We calculate these statistics using only the training data.</p>
<p>We use the same values for validation and test data. We don't recalculate these statistics from the validation or test sets, because that would allow information from those sets to influence preprocessing.</p>
<p>For Lumina, the training-data statistics are a mean of <code>96.88</code> and a standard deviation of <code>79.00</code>. We store these values in the configuration and use the same preprocessing during training, validation, and inference.</p>
<h2 id="heading-step-4-build-the-3d-segmentation-network">Step 4: Build the 3D Segmentation Network</h2>
<p>Here, we'll use DynUNet from MONAI.</p>
<p>DynUNet is a 3D U-Net architecture based on ideas from nnU-Net and supports configurable network depth, kernels, strides, and residual connections.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69fd77e89f93a850a46d376f/bad771b7-b8bb-4fc8-ab33-e17d95684419.png" alt="Simplified DynUNet architecture showing the encoder, decoder, bottleneck, and skip connections used to preserve spatial information." style="display: block;" width="600" height="400" loading="lazy">

<p>A U-Net has two main parts.</p>
<ul>
<li><p>The encoder gradually reduces the spatial resolution while learning increasingly high-level features.</p>
</li>
<li><p>The decoder then restores the spatial resolution to produce a segmentation map.</p>
</li>
</ul>
<p>Skip connections pass fine spatial details from the encoder to the decoder. This helps the decoder recover the tumor's boundary when it creates the final segmentation.</p>
<p>For Lumina, the network receives three channels:</p>
<ul>
<li><p>CT</p>
</li>
<li><p>RECIST line</p>
</li>
<li><p>Endpoint heatmap.</p>
</li>
</ul>
<p>The model configuration is:</p>
<pre><code class="language-python">from monai.networks.nets import DynUNet

# 5 levels; the first downsample skips z (see below).
kernels = [[3, 3, 3]] * 5
strides = [[1, 1, 1], [1, 2, 2], [2, 2, 2], [2, 2, 2], [2, 2, 2]]

model = DynUNet(
    spatial_dims=3,
    in_channels=3,          # CT + line + endpoint blobs
    out_channels=1,         # one probability per voxel
    kernel_size=kernels,
    strides=strides,
    upsample_kernel_size=strides[1:],
    filters=features,
    res_block=True,
)
</code></pre>
<p>The <code>strides</code> list is an important part of the configuration. Our voxels have different sizes: they're 2.5 mm apart between slices but only 1.0 mm apart within each slice. If we reduce all three dimensions at every level, we lose inter-slice detail too quickly. We'll therefore keep the z dimension unchanged during the first downsampling step. This helps preserve useful information in all three directions.</p>
<p>Lumina's main design work focuses on input representation, preprocessing, training strategy, post-processing, and CPU inference behavior.</p>
<h2 id="heading-step-5-use-a-loss-function-that-includes-boundary-information">Step 5: Use a Loss Function That Includes Boundary Information</h2>
<p>During training, the network needs a way to measure how different its prediction is from the ground-truth segmentation. This measurement is called a <strong>loss function</strong>. The network adjusts its weights to reduce this loss.</p>
<p>For Lumina, we combine two common losses: <strong>Dice loss</strong> and <strong>binary cross-entropy (BCE)</strong>.</p>
<h3 id="heading-dice-loss">Dice Loss</h3>
<p>Dice loss focuses on the overlap between the predicted tumor region and the ground-truth region.</p>
<p>It's useful when we want the overall size and shape of the predicted region to match the reference segmentation.</p>
<h3 id="heading-binary-cross-entropy">Binary Cross-entropy</h3>
<p>Binary cross-entropy works at the voxel level. For every voxel, the network predicts a probability between 0 and 1.</p>
<p>The ground-truth value is:</p>
<ul>
<li><p>1 if the voxel belongs to the tumor</p>
</li>
<li><p>0 if the voxel belongs to the background.</p>
</li>
</ul>
<p>BCE penalizes the network when its predicted probability differs from the ground-truth value.</p>
<p>For example, if a tumor voxel has a predicted probability close to<code>1</code>, the penalty is small. If the network confidently predicts a tumor voxel as background, the penalty is larger.</p>
<p>We combine the two losses by adding them:</p>
<pre><code class="language-python">loss = dice_loss + bce_loss
</code></pre>
<p>The two terms provide different training signals:</p>
<ul>
<li><p>Dice loss: encourages good overall region overlap.</p>
</li>
<li><p>BCE: encourages correct individual voxel predictions.</p>
</li>
</ul>
<h3 id="heading-adding-boundary-information">Adding Boundary Information</h3>
<p>Dice and BCE don't give the lesion boundary any special treatment.</p>
<p>This matters because a segmentation can have good overall overlap while still having an inaccurate boundary.</p>
<p>Lumina is evaluated using both:</p>
<ul>
<li><p><strong>Dice</strong>, which measures region overlap</p>
</li>
<li><p><strong>NSD (Normalized Surface Dice)</strong>, which measures surface agreement within a specified distance</p>
</li>
</ul>
<p>The official challenge tolerance is <strong>1 mm</strong>.</p>
<p>To give the network additional information about the boundary, we add a boundary-band loss term.</p>
<p>We create a thin band around the ground-truth surface by expanding and shrinking the target mask:</p>
<pre><code class="language-python">shell = dilate(target) &amp; ~erode(target)

loss = (
    dice_loss
    + bce_loss
    + 0.5 * bce_loss_on(shell)
)
</code></pre>
<p>The three terms therefore have different roles:</p>
<ul>
<li><p>Dice loss: overall region overlap</p>
</li>
<li><p>BCE: voxel-level prediction accuracy</p>
</li>
<li><p>Boundary-band loss: extra attention to voxels near the lesion surface.</p>
</li>
</ul>
<p>The additional boundary term is weighted by 0.5.</p>
<h3 id="heading-comparing-boundary-losses">Comparing Boundary Losses</h3>
<p>MONAI also provides <code>HausdorffDTLoss</code>, which uses a distance-transform-based formulation.</p>
<p>We can compare the computational cost of the different approaches during training:</p>
<table>
<thead>
<tr>
<th>Loss</th>
<th>Time per step</th>
</tr>
</thead>
<tbody><tr>
<td>Dice + cross-entropy</td>
<td>7 ms</td>
</tr>
<tr>
<td>Ours (boundary band)</td>
<td>203 ms</td>
</tr>
<tr>
<td>MONAI HausdorffDTLoss</td>
<td>957 ms</td>
</tr>
</tbody></table>
<p>The fast path for the Hausdorff distance loss required the <code>cupy</code> library, which couldn't be built in our training environment. Its CPU implementation was considerably more expensive.</p>
<p>Our boundary-band approach uses morphological operations based on max-pooling and has a lower computational cost.</p>
<p>The boundary term improved Dice on large tumors by <code>0.0136</code>. On small tumors, the results were mixed: 15 cases improved and 13 worsened.</p>
<h2 id="heading-step-6-transform-the-probability-map-into-a-segmentation-mask">Step 6: Transform the Probability Map into a Segmentation Mask</h2>
<p>The segmentation network produces a probability map. Each voxel in this map contains a value between 0 and 1, representing how likely that voxel is to belong to the tumor. We can now convert this probability map into the final 3D segmentation mask. There are three steps:</p>
<ol>
<li><p>Resample the probability map back to the original CT grid.</p>
</li>
<li><p>Apply a threshold to create a binary mask.</p>
</li>
<li><p>Keep the connected component associated with the RECIST line.</p>
</li>
</ol>
<h3 id="heading-resample-the-probability-map">Resample the Probability Map</h3>
<p>During preprocessing, we crop and resample the CT image to a fixed size of <code>64 × 160 × 160</code> voxels. The network therefore produces its prediction on this processed grid.</p>
<p>Before creating the final mask, we resample the <strong>probability map</strong> back to the original CT image grid.</p>
<pre><code class="language-python">prob_original = resample_to_original_grid(
    probability_map,
    original_image
)
</code></pre>
<p>We do this before thresholding so that the probability values can be interpolated on the original grid. This allows the final boundary to be represented more accurately.</p>
<h3 id="heading-apply-the-threshold">Apply the Threshold</h3>
<p>The network produces a probability between <code>0</code> and <code>1</code> for every voxel. We use a threshold of <code>0.35</code> to convert this probability map into a binary segmentation mask.</p>
<pre><code class="language-python">mask = prob_original &gt;= 0.35
</code></pre>
<p>Voxels with a probability of at least <code>0.35</code> become part of the predicted tumor, while the remaining voxels are treated as background.</p>
<p>The threshold of <code>0.35</code> was selected during Lumina's validation experiments and is part of the final inference pipeline.</p>
<h3 id="heading-keep-the-tumor-connected-to-the-recist-line">Keep the Tumor Connected to the RECIST Line</h3>
<p>The thresholded mask can contain small disconnected regions. Some of these regions may not belong to the tumor.</p>
<p>Because we know where the tumor was marked, we use the RECIST line to select the relevant connected component.</p>
<pre><code class="language-python">components = connected_components(mask)

tumor_mask = select_component(
    components,
    recist_line
)
</code></pre>
<p>We retain the component that intersects the RECIST line as the final tumor segmentation.</p>
<p>If the RECIST line doesn't intersect any component after thresholding, we use the component closest to the line midpoint as a fallback.</p>
<p>The order of these operations is important:</p>
<pre><code class="language-plaintext">Probability map
      ↓
Resample to original CT grid
      ↓
Apply threshold (0.35)
      ↓
Connected-component selection
      ↓
Final 3D tumor mask
</code></pre>
<h3 id="heading-why-do-we-resample-before-thresholding">Why Do We Resample Before Thresholding?</h3>
<p>We resample the <strong>probability map before thresholding</strong> so that the probability values can be interpolated on the original CT grid. During development, we also tested thresholding before resampling. That approach produced small changes at the lesion boundary because interpolation was applied to an already binary mask.</p>
<p>Using the probability map, preserves more information during interpolation and gives the final segmentation a more precise boundary.</p>
<p>The result is a 3D binary mask aligned with the original CT scan, ready for evaluation or visualization.</p>
<h2 id="heading-step-7-accelerate-and-ensure-consistency-in-cpu-inference">Step 7: Accelerate and Ensure Consistency in CPU Inference</h2>
<p>The CPU and memory limits influence the inference design.</p>
<p>We use several techniques to keep inference within the challenge constraints.</p>
<h3 id="heading-pin-the-thread-count">Pin the Thread Count</h3>
<p>CPU operations can produce small numerical differences when calculations run with different thread counts.</p>
<p>Near a segmentation threshold such as <code>0.35</code>, very small changes in probability can affect whether a voxel is included in the final mask.</p>
<p>We therefore explicitly set the PyTorch thread count:</p>
<pre><code class="language-plaintext">torch.set_num_threads(8)
</code></pre>
<p>Using the same thread configuration across runs helps keep the inference process reproducible.</p>
<h3 id="heading-budget-the-inference-passes">Budget the Inference Passes</h3>
<p>We use an ensemble because combining predictions from several model passes improves the segmentation results.</p>
<p>The ensemble uses predictions from different inputs, including flipped versions of the image and a separately trained SegResNet model.</p>
<p>The number of lesions can vary from one scan to another. If we use four passes for every lesion, a scan with five lesions requires 20 model passes. This can exceed the challenge's runtime limit.</p>
<p>We therefore set a maximum number of passes for each scan and adjust the number of passes based on the number of marked lesions.</p>
<p>A simplified version is:</p>
<pre><code class="language-plaintext">want = max(
    1,
    min(len(members) + 1, cap // max(len(ids), 1))
)
</code></pre>
<table>
<thead>
<tr>
<th>Number of tumors</th>
<th>Passes per tumor</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>4</td>
</tr>
<tr>
<td>2</td>
<td>3</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
</tr>
<tr>
<td>More than 3</td>
<td>1</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/69fd77e89f93a850a46d376f/5c02d6d1-2f89-45e3-a5d6-06524acdb5ea.png" alt="5c02d6d1-2f89-45e3-a5d6-06524acdb5ea" style="display: block;" width="600" height="400" loading="lazy">

<p>The plan is determined by the number of markers before inference starts. This keeps the amount of computation predictable and avoids making the result depend on how much time happens to remain during execution.</p>
<p>The measured results were:</p>
<table>
<thead>
<tr>
<th>Setting</th>
<th>Score</th>
</tr>
</thead>
<tbody><tr>
<td>No ensemble</td>
<td>0.7242</td>
</tr>
<tr>
<td>Cap 4</td>
<td>0.7324</td>
</tr>
<tr>
<td>Cap 6</td>
<td>0.7361</td>
</tr>
<tr>
<td>No cap (4 passes always)</td>
<td>0.7410</td>
</tr>
</tbody></table>
<p>The unrestricted ensemble produced the highest score but didn't satisfy the runtime constraint. A cap of 6 retained much of the ensemble improvement while keeping inference within the required limit.</p>
<h3 id="heading-handle-individual-failures">Handle Individual Failures</h3>
<p>When processing multiple cases, one failed case shouldn't stop the entire pipeline.</p>
<p>If a case can't be processed, we generate an empty mask and log the error. The pipeline then continues with the remaining cases.</p>
<p>This allows the complete batch to finish even when an individual case has a problem.</p>
<h2 id="heading-the-results">The Results</h2>
<p>On the 217 hidden test scans, Lumina achieved:</p>
<ul>
<li><p>Dice: 0.7619</p>
</li>
<li><p>NSD: 0.6094</p>
</li>
</ul>
<p>The NSD value uses the official <strong>1 mm tolerance</strong>.</p>
<p>The median inference time per case was <strong>20.3 seconds</strong>. The slowest observed runtime was <strong>31.8 seconds</strong>.</p>
<p>Peak memory usage was <strong>2.22 GB</strong> inside the 8 GB container.</p>
<p>The Docker image was approximately <strong>559 MB</strong>.</p>
<h3 id="heading-qualitative-results">Qualitative results</h3>
<h4 id="heading-large-lesion-with-a-clear-boundary">Large lesion with a clear boundary</h4>
<p>The first example is a large lesion with a volume of 272.9 cm³. It achieved:</p>
<ul>
<li><p>DSC: 0.961</p>
</li>
<li><p>NSD: 0.850</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69fd77e89f93a850a46d376f/62db2be9-9f49-4aa2-a416-5def38dc651f.png" alt="CT images of a well-circumscribed large lesion with a clear boundary; predicted segmentation closely follows the reference, with DSC 0.961 and NSD 0.850." style="display: block;" width="600" height="400" loading="lazy">

<p>The lesion is well circumscribed, with a clear interface with the surrounding fat.</p>
<p>The predicted contour follows the reference boundary closely across the visible extent of the lesion.</p>
<h4 id="heading-medium-lesion-with-an-uncertain-boundary">Medium lesion with an uncertain boundary</h4>
<p>The second example is a medium lesion with a volume of 28.4 cm³. It achieved:</p>
<ul>
<li><p>DSC: 0.563</p>
</li>
<li><p>NSD: 0.114</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69fd77e89f93a850a46d376f/b680de43-a438-41c4-a31d-7465577db343.png" alt="CT images of a medium lesion where the prediction covers a larger region than the reference annotation; the boundary is unclear, with DSC 0.563 and NSD 0.114." style="display: block;" width="600" height="400" loading="lazy">

<p>The prediction covers a larger area than the reference annotation.</p>
<p>Radiologist review indicated that this type of case can have uncertainty in the reference boundary itself. Therefore, a low numerical score doesn't by itself establish that the predicted contour is clinically unacceptable.</p>
<h2 id="heading-what-the-radiologist-review-showed">What the Radiologist Review Showed</h2>
<p>We also reviewed 30 lesions with a radiologist to better understand the types of errors Lumina made.</p>
<p>The review showed that lesion size wasn't the only factor affecting segmentation.</p>
<p>Lesions with clear boundaries were generally easier to segment. More difficult cases had poorly defined margins or a similar appearance to the surrounding tissue.</p>
<p>The main errors were:</p>
<ul>
<li><p>Under-segmentation: part of the lesion was missed.</p>
</li>
<li><p>Over-segmentation: surrounding tissue was included.</p>
</li>
<li><p>Boundary errors: the predicted contour did not follow an unclear or infiltrative margin.</p>
</li>
</ul>
<p>The review also showed that numerical metrics should be interpreted together with the visible lesion boundary.</p>
<p>In some cases, the reference boundary itself was difficult to define. A low score therefore didn't necessarily mean that the predicted contour was clinically unacceptable.</p>
<p>This is consistent with the lower surface accuracy observed in difficult lesions, where small boundary differences can strongly affect NSD.</p>
<h2 id="heading-three-lessons-that-apply-to-other-projects">Three Lessons That Apply to Other Projects</h2>
<h3 id="heading-1-make-your-validation-data-representative">1. Make Your Validation Data Representative</h3>
<p>A validation set should reflect the distribution that the final system will be evaluated on.</p>
<p>Our internal validation split came from the training data. Its median tumor volume was <strong>814 mm³</strong>.</p>
<p>The competition scoring set had a median tumor volume of <strong>16,805 mm³</strong>, which was much larger.</p>
<p>This difference affected how some improvements appeared during development.</p>
<p>For example, the boundary loss showed little improvement on the internal validation set for several epochs. On the public validation distribution, which contained larger lesions, its effect was more useful.</p>
<p>If an improvement targets a particular part of the data distribution, that distribution should be represented in validation.</p>
<h3 id="heading-2-measure-the-limits-of-your-preprocessing">2. Measure the Limits of Your Preprocessing</h3>
<p>The 160 mm crop was an important design choice in Lumina.</p>
<p>Some large lesions extended beyond this field of view.</p>
<p>Before changing the crop size, we measured the error introduced by cropping and resampling.</p>
<p>We passed the ground-truth masks through the same crop-and-resample pipeline and measured the resulting surface score.</p>
<p>For large lesions, the geometry ceiling was <strong>0.9699 NSD</strong>, while the model achieved <strong>0.5353 NSD</strong>.</p>
<p>This showed that the preprocessing pipeline preserved the lesion surface reasonably well. Preprocessing alone did not explain the remaining gap.</p>
<p>We also tested several approaches for increasing the field of view, including:</p>
<ul>
<li><p>Overlapping tiles</p>
</li>
<li><p>Adaptive zoom</p>
</li>
<li><p>A larger crop</p>
</li>
</ul>
<p>These changes didn't improve the final score.</p>
<p>Measuring the preprocessing ceiling helped us focus further development on the model and inference pipeline.</p>
<h3 id="heading-3-record-where-your-numbers-come-from">3. Record Where Your Numbers Come From</h3>
<p>Model development involves many configuration values:</p>
<ul>
<li><p>Thresholds</p>
</li>
<li><p>Crop sizes</p>
</li>
<li><p>Voxel spacing</p>
</li>
<li><p>Loss weights</p>
</li>
<li><p>Sampling weights</p>
</li>
<li><p>Ensemble settings</p>
</li>
<li><p>Runtime limits</p>
</li>
</ul>
<p>Each value should have a clear source.</p>
<p>For example, record:</p>
<ul>
<li><p>What was measured</p>
</li>
<li><p>Which dataset was used</p>
</li>
<li><p>When the measurement was made</p>
</li>
<li><p>Which alternatives were tested</p>
</li>
<li><p>Why the final value was selected.</p>
</li>
</ul>
<p>This makes it easier to reproduce experiments and understand design decisions later.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Lumina shows that a single RECIST line can reconstruct a tumor's 3D extent from a CT scan.</p>
<p>The system combines prompt-based input channels, a fixed physical crop, a 3D DynUNet, boundary-aware training, probability-based post-processing, and controlled CPU inference.</p>
<p>On the 217 held-out cases, Lumina achieved a <strong>Dice score of 0.7619</strong> and an <strong>NSD of 0.6094</strong> at the official 1 mm tolerance.</p>
<p>The median inference time was <strong>20.3 seconds</strong>, with a peak memory usage of <strong>2.22 GB</strong>, keeping the system within the challenge constraints.</p>
<p>The experiments also showed that boundary accuracy remains an important area for improvement, particularly for large and less clearly defined lesions.</p>
<p>The geometry-ceiling analysis showed that the crop and resampling pipeline preserved the lesion surface well, indicating that further improvements should focus on segmentation accuracy and robustness.</p>
<p>Overall, Lumina demonstrates a practical approach for converting a 2D RECIST measurement into a 3D lesion segmentation while meeting strict computational constraints.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
