Skip to content

Preprocessing API

Preprocessing utilities for image manipulation and sentinel value handling.

Image Resampling

pictologics.preprocessing.resample_image(image, new_spacing, interpolation='linear', boundary_mode='nearest', round_intensities=False, mask_threshold=None, source_mask=None)

Resample image to new voxel spacing using IBSI-compliant 'Align grid centers' method.

Uses scipy.ndimage.affine_transform for memory efficiency.

Parameters:

Name Type Description Default
image Image

Input Image object.

required
new_spacing tuple[float, float, float]

Target spacing (x, y, z). Must be positive.

required
interpolation str

Interpolation method. 'nearest': Nearest neighbour (order 0). 'linear': Trilinear (order 1). 'cubic': Tricubic spline (order 3).

'linear'
boundary_mode str

Padding mode for extrapolation. 'nearest' (default): Replicates edge values (aaaa|abcd|dddd). 'constant': Pads with constant value (0). 'reflect': Reflects at boundary. 'wrap': Wraps around.

'nearest'
round_intensities bool

If True, round resulting intensities to nearest integer.

False
mask_threshold Optional[float]

If provided, treat output as a binary mask. Values >= threshold become 1, others 0. Commonly 0.5 for partial volume correction.

None
source_mask Optional[Image | NDArray[bool_]]

Optional source validity mask. If provided (or if image.source_mask is set), only valid voxels are used for interpolation. This prevents sentinel values (e.g., -2048 in CT) from contaminating the resampled output. Can be an Image object or a boolean numpy array.

None

Returns:

Type Description
Image

Resampled Image object. If source_mask was used, the output Image will have

Image

its source_mask attribute set to the resampled validity mask.

Note

When source_mask is active, the function uses normalized interpolation: the contribution of each input voxel is weighted by its validity, and the result is normalized by the sum of valid weights. This ensures that sentinel voxels do not affect the output.

Example

Resample image to isotropic 1mm spacing using linear interpolation:

from pictologics.preprocessing import resample_image

# Resample to 1x1x1 mm
resampled_img = resample_image(
    image,
    new_spacing=(1.0, 1.0, 1.0),
    interpolation="linear"
)

Resample with sentinel-value exclusion:

# Image has -2048 outside ROI
image_with_sentinel = image.with_source_mask(roi_mask)
resampled = resample_image(
    image_with_sentinel,
    new_spacing=(1.0, 1.0, 1.0)
)
# Sentinel voxels were excluded from interpolation
Source code in pictologics/preprocessing.py
def resample_image(
    image: Image,
    new_spacing: tuple[float, float, float],
    interpolation: str = "linear",
    boundary_mode: str = "nearest",
    round_intensities: bool = False,
    mask_threshold: Optional[float] = None,
    source_mask: Optional[Image | npt.NDArray[np.bool_]] = None,
) -> Image:
    """
    Resample image to new voxel spacing using IBSI-compliant 'Align grid centers' method.

    Uses scipy.ndimage.affine_transform for memory efficiency.

    Args:
        image: Input Image object.
        new_spacing: Target spacing (x, y, z). Must be positive.
        interpolation: Interpolation method.
            'nearest': Nearest neighbour (order 0).
            'linear': Trilinear (order 1).
            'cubic': Tricubic spline (order 3).
        boundary_mode: Padding mode for extrapolation.
            'nearest' (default): Replicates edge values (aaaa|abcd|dddd).
            'constant': Pads with constant value (0).
            'reflect': Reflects at boundary.
            'wrap': Wraps around.
        round_intensities: If True, round resulting intensities to nearest integer.
        mask_threshold: If provided, treat output as a binary mask.
                        Values >= threshold become 1, others 0.
                        Commonly 0.5 for partial volume correction.
        source_mask: Optional source validity mask. If provided (or if image.source_mask
                     is set), only valid voxels are used for interpolation. This prevents
                     sentinel values (e.g., -2048 in CT) from contaminating the resampled
                     output. Can be an Image object or a boolean numpy array.

    Returns:
        Resampled Image object. If source_mask was used, the output Image will have
        its source_mask attribute set to the resampled validity mask.

    Note:
        When source_mask is active, the function uses normalized interpolation:
        the contribution of each input voxel is weighted by its validity, and the
        result is normalized by the sum of valid weights. This ensures that sentinel
        voxels do not affect the output.

    Example:
        Resample image to isotropic 1mm spacing using linear interpolation:

        ```python
        from pictologics.preprocessing import resample_image

        # Resample to 1x1x1 mm
        resampled_img = resample_image(
            image,
            new_spacing=(1.0, 1.0, 1.0),
            interpolation="linear"
        )
        ```

        Resample with sentinel-value exclusion:

        ```python
        # Image has -2048 outside ROI
        image_with_sentinel = image.with_source_mask(roi_mask)
        resampled = resample_image(
            image_with_sentinel,
            new_spacing=(1.0, 1.0, 1.0)
        )
        # Sentinel voxels were excluded from interpolation
        ```
    """
    if any(s <= 0 for s in new_spacing):
        raise ValueError(f"New spacing must be positive, got {new_spacing}")

    # Determine effective source mask
    effective_source: Optional[npt.NDArray[np.bool_]] = None

    if source_mask is not None:
        if isinstance(source_mask, Image):
            effective_source = source_mask.array > 0
        else:
            effective_source = source_mask.astype(bool)
    elif image.has_source_mask:
        effective_source = image.source_mask

    # Map interpolation string to spline order
    interpolation_map = {
        "nearest": 0,
        "linear": 1,
        "cubic": 3,
    }

    if interpolation not in interpolation_map:
        raise ValueError(
            f"Unknown interpolation method: {interpolation}. "
            f"Supported: {list(interpolation_map.keys())}"
        )

    order = interpolation_map[interpolation]

    # Calculate new shape
    # IBSI: nb = ceil(na * sa / sb)
    original_spacing = np.array(image.spacing)
    target_spacing = np.array(new_spacing)

    # Scale factor for dimensions (how many new voxels per old voxel)
    # dim_scale = s_old / s_new
    dim_scale = original_spacing / target_spacing

    new_shape = np.ceil(image.array.shape * dim_scale).astype(int)

    # Calculate affine transform parameters
    # We map Output Coordinate (x_out) -> Input Coordinate (x_in)
    # x_in = matrix * x_out + offset

    # Scale factor for coordinates (step size in input space per step in output space)
    # step_in = s_new / s_old
    coord_scale = target_spacing / original_spacing
    matrix = coord_scale  # Diagonal matrix elements

    # Calculate offset for 'Align Grid Centers
    center_orig = (np.array(image.array.shape) - 1) / 2.0
    center_new = (new_shape - 1) / 2.0

    offset = center_orig - matrix * center_new

    # Perform resampling
    new_source_mask: Optional[npt.NDArray[np.bool_]] = None

    if effective_source is None:
        # Original behavior - use all voxels
        resampled_array = affine_transform(
            image.array,
            matrix=matrix,
            offset=offset,
            output_shape=tuple(new_shape),
            order=order,
            mode=boundary_mode,
        )
    else:
        # Masked resampling using normalized interpolation
        resampled_array, new_source_mask = _resample_with_source_mask(
            image.array,
            effective_source,
            matrix=matrix,
            offset=offset,
            output_shape=tuple(new_shape),
            order=order,
            mode=boundary_mode,
        )

    # Post-processing
    if mask_threshold is not None:
        # Binarize mask
        resampled_array = (resampled_array >= mask_threshold).astype(np.uint8)
    elif round_intensities:
        # Round intensities
        resampled_array = np.round(resampled_array)

    # Update origin to maintain center alignment
    # O_new = O_old + 0.5 * ( (N_old-1)*S_old - (N_new-1)*S_new )
    extent_orig = (np.array(image.array.shape) - 1) * original_spacing
    extent_new = (new_shape - 1) * target_spacing
    origin_shift = 0.5 * (extent_orig - extent_new)
    new_origin = tuple(np.array(image.origin) + origin_shift)

    return Image(
        array=resampled_array,
        spacing=new_spacing,
        origin=new_origin,
        direction=image.direction,
        modality=image.modality,
        source_mask=new_source_mask,
    )

Discretisation

pictologics.preprocessing.discretise_image(image, method, roi_mask=None, n_bins=None, bin_width=None, min_val=None, max_val=None, cutoffs=None)

Discretise image intensities.

Supports IBSI-compliant Fixed Bin Number (FBN) and Fixed Bin Size (FBS).

Parameters:

Name Type Description Default
image Image | NDArray[floating[Any]]

Input Image object or numpy array.

required
method str

'FBN' (Fixed Bin Number), 'FBS' (Fixed Bin Size), or 'FIXED_CUTOFFS'.

required
roi_mask Image | NDArray[floating[Any]] | None

Optional mask to define the ROI for determining min/max values.

None
n_bins Optional[int]

Number of bins (required for FBN).

None
bin_width Optional[float]

Bin width (required for FBS).

None
min_val Optional[float]

Minimum value for discretisation. For FBS, defaults to ROI minimum (or global minimum). For FBN, defaults to ROI minimum.

None
max_val Optional[float]

Maximum value for discretisation (FBN only). Defaults to ROI maximum.

None
cutoffs Optional[list[float]]

List of cutoffs (required for FIXED_CUTOFFS).

None

Returns:

Type Description
Image | NDArray[floating[Any]]

Discretised Image object or numpy array (depending on input).

Image | NDArray[floating[Any]]

Values are 1-based indices.

Example

Discretise image into 32 fixed bins (FBN):

from pictologics.preprocessing import discretise_image

# FBN with 32 bins
disc_image = discretise_image(
    image,
    method="FBN",
    n_bins=32
)
Source code in pictologics/preprocessing.py
def discretise_image(
    image: Image | npt.NDArray[np.floating[Any]],
    method: str,
    roi_mask: Image | npt.NDArray[np.floating[Any]] | None = None,
    n_bins: Optional[int] = None,
    bin_width: Optional[float] = None,
    min_val: Optional[float] = None,
    max_val: Optional[float] = None,
    cutoffs: Optional[list[float]] = None,
) -> Image | npt.NDArray[np.floating[Any]]:
    """
    Discretise image intensities.

    Supports IBSI-compliant Fixed Bin Number (FBN) and Fixed Bin Size (FBS).

    Args:
        image: Input Image object or numpy array.
        method: 'FBN' (Fixed Bin Number), 'FBS' (Fixed Bin Size), or 'FIXED_CUTOFFS'.
        roi_mask: Optional mask to define the ROI for determining min/max values.
        n_bins: Number of bins (required for FBN).
        bin_width: Bin width (required for FBS).
        min_val: Minimum value for discretisation.
                 For FBS, defaults to ROI minimum (or global minimum).
                 For FBN, defaults to ROI minimum.
        max_val: Maximum value for discretisation (FBN only).
                 Defaults to ROI maximum.
        cutoffs: List of cutoffs (required for FIXED_CUTOFFS).

    Returns:
        Discretised Image object or numpy array (depending on input).
        Values are 1-based indices.

    Example:
        Discretise image into 32 fixed bins (FBN):

        ```python
        from pictologics.preprocessing import discretise_image

        # FBN with 32 bins
        disc_image = discretise_image(
            image,
            method="FBN",
            n_bins=32
        )
        ```
    """
    # Handle input type
    if isinstance(image, Image):
        array = image.array
        is_image_obj = True
    else:
        array = image
        is_image_obj = False

    # Determine ROI values for default min/max
    if roi_mask is not None:
        if isinstance(roi_mask, Image):
            mask_arr = roi_mask.array
        else:
            mask_arr = roi_mask

        if mask_arr.shape != array.shape:
            raise ValueError(
                f"Shape mismatch: Image {array.shape} vs Mask {mask_arr.shape}"
            )

        # Extract ROI values (ignoring NaNs)
        roi_values = array[(mask_arr > 0) & (~np.isnan(array))]
    else:
        roi_values = array[~np.isnan(array)]

    # Initialize result
    discretised = np.zeros(array.shape, dtype=int)

    # We process all non-NaN pixels in the image
    valid_mask = ~np.isnan(array)
    values = array[valid_mask]

    if values.size == 0:
        if is_image_obj:
            # Create new Image with discretised array
            return Image(
                array=discretised,
                spacing=image.spacing,  # type: ignore
                origin=image.origin,  # type: ignore
                direction=image.direction,  # type: ignore
                modality=image.modality,  # type: ignore
            )
        return discretised

    if method == "FBN":
        if n_bins is None:
            raise ValueError("n_bins required for FBN")
        if n_bins <= 0:
            raise ValueError("n_bins must be positive")

        # Determine min/max
        current_min = min_val
        if current_min is None:
            current_min = np.min(roi_values) if roi_values.size > 0 else np.min(values)

        current_max = max_val
        if current_max is None:
            current_max = np.max(roi_values) if roi_values.size > 0 else np.max(values)

        if current_max <= current_min:
            # Edge case: flat region or invalid range
            discretised[valid_mask] = 1
        else:
            # IBSI FBN: floor(N_g * (X - X_min) / (X_max - X_min)) + 1
            temp_discretised = (
                np.floor(n_bins * (values - current_min) / (current_max - current_min))
                + 1
            )

            # Handle max value case (it falls into N_g + 1 with this formula)
            # Also clip outliers
            temp_discretised[values >= current_max] = n_bins
            temp_discretised = np.clip(temp_discretised, 1, n_bins)

            discretised[valid_mask] = temp_discretised.astype(int)

    elif method == "FBS":
        if bin_width is None:
            raise ValueError("bin_width required for FBS")
        if bin_width <= 0:
            raise ValueError("bin_width must be positive")

        current_min = min_val
        if current_min is None:
            current_min = np.min(roi_values) if roi_values.size > 0 else np.min(values)

        # IBSI FBS: floor((X - X_min) / w_b) + 1
        temp_discretised = np.floor((values - current_min) / bin_width) + 1

        # Ensure minimum bin is 1
        temp_discretised[temp_discretised < 1] = 1
        discretised[valid_mask] = temp_discretised.astype(int)

    elif method == "FIXED_CUTOFFS":
        if cutoffs is None:
            raise ValueError("cutoffs required for FIXED_CUTOFFS")

        temp_discretised = np.digitize(values, bins=np.array(cutoffs))
        discretised[valid_mask] = temp_discretised.astype(int)

    else:
        raise ValueError(f"Unknown discretisation method: {method}")

    if is_image_obj:
        return Image(
            array=discretised,
            spacing=image.spacing,  # type: ignore
            origin=image.origin,  # type: ignore
            direction=image.direction,  # type: ignore
            modality=image.modality,  # type: ignore
        )
    return discretised

Mask Operations

pictologics.preprocessing.apply_mask(image, mask, mask_values=1)

Apply mask to image and return flattened array of voxel values.

Parameters:

Name Type Description Default
image Image | NDArray[floating[Any]]

Image object or numpy array.

required
mask Image | NDArray[floating[Any]]

Image object (mask) or numpy array.

required
mask_values int | list[int] | None

Value(s) in the mask to consider as ROI. Default is 1. Can be a single integer or a list of integers.

1

Returns:

Type Description
NDArray[floating[Any]]

1D numpy array of values within the mask.

Source code in pictologics/preprocessing.py
def apply_mask(
    image: Image | npt.NDArray[np.floating[Any]],
    mask: Image | npt.NDArray[np.floating[Any]],
    mask_values: int | list[int] | None = 1,
) -> npt.NDArray[np.floating[Any]]:
    """
    Apply mask to image and return flattened array of voxel values.

    Args:
        image: Image object or numpy array.
        mask: Image object (mask) or numpy array.
        mask_values: Value(s) in the mask to consider as ROI. Default is 1.
                     Can be a single integer or a list of integers.

    Returns:
        1D numpy array of values within the mask.
    """
    # Handle inputs
    img_arr = image.array if isinstance(image, Image) else image
    mask_arr = mask.array if isinstance(mask, Image) else mask

    # Ensure shapes match
    if img_arr.shape != mask_arr.shape:
        raise ValueError(
            f"Image shape {img_arr.shape} and mask shape {mask_arr.shape} do not match"
        )

    # Handle mask values
    if mask_values is None:
        mask_values = [1]
    elif isinstance(mask_values, int):
        mask_values = [mask_values]

    # Create boolean mask
    roi_mask = np.isin(mask_arr, mask_values)

    if not np.any(roi_mask):
        return np.array([])

    # Apply mask
    return img_arr[roi_mask]

pictologics.preprocessing.resegment_mask(image, mask, range_min=None, range_max=None)

Update mask to exclude voxels where image intensity is outside the specified range. Used for IBSI re-segmentation (e.g. [-1000, 400] HU).

Parameters:

Name Type Description Default
image Image

Image object.

required
mask Image

Image object (mask).

required
range_min Optional[float]

Minimum intensity value (inclusive). If None, no lower bound.

None
range_max Optional[float]

Maximum intensity value (inclusive). If None, no upper bound.

None

Returns:

Type Description
Image

Updated Image object (mask) with re-segmentation applied.

Example

Resegment mask to keep only values between -1000 and 400 (e.g. HU range):

from pictologics.preprocessing import resegment_mask

# Keep voxels in range [-1000, 400]
new_mask = resegment_mask(
    image,
    mask,
    range_min=-1000,
    range_max=400
)
Source code in pictologics/preprocessing.py
def resegment_mask(
    image: Image,
    mask: Image,
    range_min: Optional[float] = None,
    range_max: Optional[float] = None,
) -> Image:
    """
    Update mask to exclude voxels where image intensity is outside the specified range.
    Used for IBSI re-segmentation (e.g. [-1000, 400] HU).

    Args:
        image: Image object.
        mask: Image object (mask).
        range_min: Minimum intensity value (inclusive). If None, no lower bound.
        range_max: Maximum intensity value (inclusive). If None, no upper bound.

    Returns:
        Updated Image object (mask) with re-segmentation applied.

    Example:
        Resegment mask to keep only values between -1000 and 400 (e.g. HU range):

        ```python
        from pictologics.preprocessing import resegment_mask

        # Keep voxels in range [-1000, 400]
        new_mask = resegment_mask(
            image,
            mask,
            range_min=-1000,
            range_max=400
        )
        ```
    """
    if image.array.shape != mask.array.shape:
        raise ValueError("Image and mask must have the same shape for re-segmentation.")

    new_mask_array = mask.array.copy()

    # Identify outliers
    outliers = np.zeros(image.array.shape, dtype=bool)

    if range_min is not None:
        outliers |= image.array < range_min

    if range_max is not None:
        outliers |= image.array > range_max

    # Set mask to 0 where outliers exist
    new_mask_array[outliers] = 0

    return Image(
        array=new_mask_array,
        spacing=mask.spacing,
        origin=mask.origin,
        direction=mask.direction,
        modality=mask.modality,
    )

Outlier Filtering

pictologics.preprocessing.filter_outliers(image, mask, sigma=3.0)

Exclude outliers from the mask based on mean +/- sigma * std. IBSI 3.6.

Parameters:

Name Type Description Default
image Image

Image object.

required
mask Image

Image object (mask).

required
sigma float

Number of standard deviations.

3.0

Returns:

Type Description
Image

New Image object (mask) with outliers removed.

Example

Remove outliers beyond 3 standard deviations from the mean:

from pictologics.preprocessing import filter_outliers

# Remove outliers > 3 sigma
clean_mask = filter_outliers(
    image,
    mask,
    sigma=3.0
)
Source code in pictologics/preprocessing.py
def filter_outliers(image: Image, mask: Image, sigma: float = 3.0) -> Image:
    """
    Exclude outliers from the mask based on mean +/- sigma * std.
    IBSI 3.6.

    Args:
        image: Image object.
        mask: Image object (mask).
        sigma: Number of standard deviations.

    Returns:
        New Image object (mask) with outliers removed.

    Example:
        Remove outliers beyond 3 standard deviations from the mean:

        ```python
        from pictologics.preprocessing import filter_outliers

        # Remove outliers > 3 sigma
        clean_mask = filter_outliers(
            image,
            mask,
            sigma=3.0
        )
        ```
    """
    # Extract values within the mask
    values = apply_mask(image, mask)

    if values.size == 0:
        return mask

    mean_val = np.mean(values)
    # IBSI uses population std (no bias correction, ddof=0)
    std_val = np.std(values, ddof=0)

    lower_bound = mean_val - sigma * std_val
    upper_bound = mean_val + sigma * std_val

    # Create outlier mask
    # Keep values within [lower, upper]
    valid_mask = (image.array >= lower_bound) & (image.array <= upper_bound)

    # Update original mask
    new_mask_array = mask.array.copy()

    # Ensure boolean or integer type for bitwise operation
    # Assuming mask.array is binary (0/1) or boolean
    if new_mask_array.dtype == bool:
        new_mask_array = new_mask_array & valid_mask
    else:
        new_mask_array = (new_mask_array * valid_mask).astype(np.uint8)

    return Image(
        array=new_mask_array,
        spacing=mask.spacing,
        origin=mask.origin,
        direction=mask.direction,
        modality=mask.modality,
    )

Sentinel Value Handling

Utilities for detecting and masking sentinel values (e.g., -2048 HU for outside-FOV regions in CT).

pictologics.preprocessing.detect_sentinel_value(image, candidate_values=COMMON_SENTINEL_VALUES, min_presence_fraction=0.01, roi_mask=None)

Detect if image contains a common sentinel value outside the ROI.

A sentinel value is detected if: 1. It appears in a significant fraction of voxels (>= min_presence_fraction) 2. If roi_mask is provided, the sentinel primarily appears outside the ROI

This is used by the pipeline's AUTO source_mode to automatically detect images that have been pre-masked with sentinel values.

Parameters:

Name Type Description Default
image Image

Input Image object.

required
candidate_values tuple[float, ...]

Values to check for sentinel patterns. Defaults to common medical imaging sentinels: -2048, -1024, -1000, 0, -32768.

COMMON_SENTINEL_VALUES
min_presence_fraction float

Minimum fraction of voxels that must equal the candidate to consider it a sentinel. Default is 1%.

0.01
roi_mask Optional[Image]

Optional ROI mask. If provided, checks that sentinel values are primarily outside the mask (ratio > 2:1 outside vs inside).

None

Returns:

Type Description
Optional[float]

The detected sentinel value, or None if not detected.

Example
from pictologics.preprocessing import detect_sentinel_value
from pictologics.loader import load_image

image = load_image("image_with_sentinel.nii.gz")
sentinel = detect_sentinel_value(image)

if sentinel is not None:
    print(f"Detected sentinel value: {sentinel}")
Source code in pictologics/preprocessing.py
def detect_sentinel_value(
    image: Image,
    candidate_values: tuple[float, ...] = COMMON_SENTINEL_VALUES,
    min_presence_fraction: float = 0.01,
    roi_mask: Optional[Image] = None,
) -> Optional[float]:
    """
    Detect if image contains a common sentinel value outside the ROI.

    A sentinel value is detected if:
    1. It appears in a significant fraction of voxels (>= min_presence_fraction)
    2. If roi_mask is provided, the sentinel primarily appears outside the ROI

    This is used by the pipeline's AUTO source_mode to automatically detect
    images that have been pre-masked with sentinel values.

    Args:
        image: Input Image object.
        candidate_values: Values to check for sentinel patterns.
            Defaults to common medical imaging sentinels: -2048, -1024, -1000, 0, -32768.
        min_presence_fraction: Minimum fraction of voxels that must equal
            the candidate to consider it a sentinel. Default is 1%.
        roi_mask: Optional ROI mask. If provided, checks that sentinel values
            are primarily outside the mask (ratio > 2:1 outside vs inside).

    Returns:
        The detected sentinel value, or None if not detected.

    Example:
        ```python
        from pictologics.preprocessing import detect_sentinel_value
        from pictologics.loader import load_image

        image = load_image("image_with_sentinel.nii.gz")
        sentinel = detect_sentinel_value(image)

        if sentinel is not None:
            print(f"Detected sentinel value: {sentinel}")
        ```
    """
    array = image.array
    total_voxels = array.size

    for candidate in candidate_values:
        count = np.sum(array == candidate)
        fraction = count / total_voxels

        if fraction >= min_presence_fraction:
            # If ROI mask provided, verify sentinel is primarily outside
            if roi_mask is not None:
                roi_arr = roi_mask.array > 0
                inside_count = np.sum((array == candidate) & roi_arr)
                outside_count = np.sum((array == candidate) & ~roi_arr)

                # Sentinel should be mostly outside ROI (at least 2:1 ratio)
                if outside_count > inside_count * 2:
                    return candidate
            else:
                return candidate

    return None

pictologics.preprocessing.create_source_mask_from_sentinel(image, sentinel_value, tolerance=0.0)

Create a source validity mask by marking sentinel voxels as invalid.

The returned mask has value 1 for valid (non-sentinel) voxels and 0 for invalid (sentinel) voxels. This mask can be used with the Image.source_mask attribute to exclude sentinel voxels from resampling and filtering.

Parameters:

Name Type Description Default
image Image

Input Image object.

required
sentinel_value float

The value considered as sentinel (invalid data).

required
tolerance float

Values within this tolerance of sentinel_value are also considered invalid. Default 0 means exact match only.

0.0

Returns:

Type Description
Image

Image object with binary mask (1 = valid, 0 = sentinel).

Example
from pictologics.preprocessing import create_source_mask_from_sentinel
from pictologics.loader import load_image

image = load_image("ct_with_background.nii.gz")

# Create mask excluding -2048 sentinel values
source_mask = create_source_mask_from_sentinel(image, sentinel_value=-2048)

# Apply to image for sentinel-aware processing
image_with_mask = image.with_source_mask(source_mask)
Source code in pictologics/preprocessing.py
def create_source_mask_from_sentinel(
    image: Image,
    sentinel_value: float,
    tolerance: float = 0.0,
) -> Image:
    """
    Create a source validity mask by marking sentinel voxels as invalid.

    The returned mask has value 1 for valid (non-sentinel) voxels and 0 for
    invalid (sentinel) voxels. This mask can be used with the Image.source_mask
    attribute to exclude sentinel voxels from resampling and filtering.

    Args:
        image: Input Image object.
        sentinel_value: The value considered as sentinel (invalid data).
        tolerance: Values within this tolerance of sentinel_value are also
            considered invalid. Default 0 means exact match only.

    Returns:
        Image object with binary mask (1 = valid, 0 = sentinel).

    Example:
        ```python
        from pictologics.preprocessing import create_source_mask_from_sentinel
        from pictologics.loader import load_image

        image = load_image("ct_with_background.nii.gz")

        # Create mask excluding -2048 sentinel values
        source_mask = create_source_mask_from_sentinel(image, sentinel_value=-2048)

        # Apply to image for sentinel-aware processing
        image_with_mask = image.with_source_mask(source_mask)
        ```
    """
    if tolerance == 0:
        valid = image.array != sentinel_value
    else:
        valid = np.abs(image.array - sentinel_value) > tolerance

    return Image(
        array=valid.astype(np.uint8),
        spacing=image.spacing,
        origin=image.origin,
        direction=image.direction,
        modality="SOURCE_MASK",
    )