Example #1
0
def _img_convert_type(img: sitk.Image, output_type) -> sitk.Image:
    """
    Convert the img into the desired pixel type. The method safely ( without overflow/underflow ) converts from one pixel range to another.

    :param img: a SimpleITK Image object
    :param output_type: a SimpleITK PixelID such as sitkUInt8, sitkFloat32 for the pixel type of the returned image
    """

    # the sub_volume_execute

    if img.GetPixelID() == sitk.sitkInt8 and output_type == sitk.sitkUInt8:
        img = sitk.Cast(img, sitk.sitkInt16)
        img += 128
        return sitk.Cast(img, output_type)
    elif img.GetPixelID() == sitk.sitkInt16 and output_type == sitk.sitkUInt8:
        img = sitk.Cast(img, sitk.sitkInt32)
        img += 32768
        img /= 256
        return sitk.Cast(img, output_type)
    elif img.GetPixelID() == sitk.sitkInt16 and output_type == sitk.sitkUInt16:
        img = sitk.Cast(img, sitk.sitkInt32)
        img += 32768
        return sitk.Cast(img, output_type)
    else:
        raise Exception(
            f"Converting from {img.GetPixelIDTypeAsString()} to "
            f"{sitk.GetPixelIDValueAsString(output_type)} is not implemented.")
Example #2
0
    def execute(self, image: sitk.Image, params: ImageRegistrationParameters = None) -> sitk.Image:
        """Registers an image.

        Args:
            image (sitk.Image): The image.
            params (ImageRegistrationParameters): The registration parameters.

        Returns:
            sitk.Image: The registered image.
        """

        # todo: replace this filter by a registration. Registration can be costly, therefore, we provide you the
        # transformation, which you only need to apply to the image!
        atlas = params.atlas
        transform = params.transformation
        is_ground_truth = params.is_ground_truth  # the ground truth will be handled slightly different

        # note: if you are interested in registration, and want to test it, have a look at
        # pymia.filtering.registration.MultiModalRegistration. Think about the type of registration, i.e.
        # do you want to register to an atlas or inter-subject? Or just ask us, we can guide you ;-)

        # interpolator = sitk.sitkCosineWindowedSinc or sitk.sitkAffine
        if is_ground_truth:
            image = sitk.Resample(image, atlas, transform, sitk.sitkLinear, 0.0, image.GetPixelID())
        else:
            image = sitk.Resample(image, atlas, transform, sitk.sitkLinear, 0.0, image.GetPixelID())

        return image
Example #3
0
    def execute(self,
                image: sitk.Image,
                params: CmdlineExecutorParams = None) -> sitk.Image:
        """Executes a command line program.

        Args:
            image (sitk.Image): The image to filter.
            params (CmdlineExecutorParams): The execution specific command line parameters.

        Returns:
            sitk.Image: The filtered image.
        """
        temp_dir = tempfile.gettempdir()
        temp_in = os.path.join(temp_dir, 'in.nii')
        sitk.WriteImage(image, temp_in)
        temp_out = os.path.join(temp_dir, 'out.nii')

        cmd = [self.executable_path, temp_in, temp_out]
        if params is not None:
            cmd = cmd + params.arguments

        subprocess.run(cmd, check=True)
        out_image = sitk.ReadImage(temp_out, image.GetPixelID())

        # clean up
        os.remove(temp_in)
        os.remove(temp_out)

        return out_image
Example #4
0
def apply_transform_pandas(fixed_image: sitk.Image,
                           moving_image: sitk.Image,
                           reference_path,
                           index=None):

    transform_path = os.path.join(reference_path.parent, 'Transforms.csv')

    if index is None:
        transform_params = blk.read_pandas_row(transform_path,
                                               reference_path.name, 'Image')
    else:
        transform_params = blk.read_pandas_row(transform_path, index, 'Image')

    transform = sitk.AffineTransform(2)

    transform.Rotate(0, 1, transform_params['Rotation'], pre=True)

    matrix = [
        transform_params['Matrix Top Left'],
        transform_params['Matrix Top Right'],
        transform_params['Matrix Bottom Left'],
        transform_params['Matrix Bottom Right']
    ]

    transform.SetMatrix(matrix)
    transform.SetTranslation(
        [transform_params['X Translation'], transform_params['Y Translation']])

    origin = (int(transform_params['X Origin']),
              int(transform_params['Y Origin']))
    moving_image.SetOrigin(origin)

    return sitk.Resample(moving_image, fixed_image, transform, sitk.sitkLinear,
                         0.0, moving_image.GetPixelID())
Example #5
0
def clip_intensity(image: sitk.Image, lower: float, upper: float):
    """Clip image grey level intensities to specified range.

    The grey level intensities in the resulting image will fall in the range
    [lower, upper].

    Parameters
    ----------
    image
        The intensity image to clip.

    lower
        The lower bound on grey level intensity. Voxels with lower intensity
        will be set to this value.

    upper
        The upper bound on grey level intensity. Voxels with higer intensity
        will be set to this value.

    Returns
    -------
    sitk.Image
        The clipped intensity image.
    """
    return sitk.Clamp(image, image.GetPixelID(), lower, upper)
Example #6
0
def float_dilate(image: sitk.Image, dilate: int) -> sitk.Image:
    r""" Dilate a float valued image.

    Parameters
    ----------
    image : sitk.Image
        Image to be dilated.
    dilate : int
        Radius of the structuring element.

    Returns
    -------
    sitk.Image
        Dilated image.
    """

    if type(dilate) is not int or dilate < 1:
        raise Exception('float_dilate: invalid dilation value')

    original_type = image.GetPixelID()
    image = sitk.Cast(image, sitk.sitkUInt8)
    image = sitk.BinaryDilate(image, dilate)
    image = sitk.Cast(image, original_type)

    return image
Example #7
0
def fitting_index(image: sitk.Image,
                  norm: Union[int, str] = 2.0,
                  centre: Tuple[float, float, float] = None,
                  radius: float = None,
                  padding: bool = True) -> float:
    r""" Compute the fitting index of an input object.

    The fitting index of order `p` is defined as the Jaccard coefficient
    computed between the input object and a p-ball centred in the object's
    centre of mass.

    Parameters
    ----------
    image : sitk.Image
        Input binary image of the object.
    norm : Union[int,str]
        Order of the Minkowski norm ('inf' or 'max' to use the Chebyshev norm).
    centre : Tuple[float, float, float]
        Forces the p-ball to be centred in a specific point.
    radius : float
        Force the radius of the p-ball.
    padding : bool
        If `True`, add enough padding to be sure that the ball will entirely
        fit within the volume.

    Returns
    -------
    float
        Value of the fitting index.
    """

    if image.GetPixelID() != sitk.sitkUInt8:
        raise Exception('Unsupported %s pixel type' %
                        image.GetPixelIDTypeAsString())

    if centre is None:
        # Use the centroid as centre
        lssif = sitk.LabelShapeStatisticsImageFilter()
        lssif.Execute(image)
        centre = lssif.GetCentroid(1)

    if padding:
        # Add some padding to be sure that an isovolumetric 1-ball can fit
        # within the same volume of a sphere touching the boundary
        pad = tuple([x // 4 for x in image.GetSize()])
        image = sitk.ConstantPad(image, pad, pad, 0)
        image.SetOrigin((0, 0, 0))
        centre = tuple([x + y for x, y in zip(centre, pad)])

    if radius is None:
        radius = isovolumteric_radius(image, norm)

    size = image.GetSize()

    sphere = drawing.create_sphere(radius, size=size, centre=centre,
                                   norm=norm) > 0

    return jaccard(image, sphere)
Example #8
0
def apply_transform_fromfile(fixed_image: sitk.Image, moving_image: sitk.Image,
                             transform_path):
    transform = sitk.ReadTransform(str(transform_path))
    registered_image = sitk.Resample(moving_image, fixed_image, transform,
                                     sitk.sitkLinear, 0.0,
                                     moving_image.GetPixelID())

    meta.copy_relevant_metadata(registered_image, moving_image)

    return registered_image
def apply_bias_correction(img:sitk.Image):
    # print('working on N4')
    initial_img = img
    img_size = initial_img.GetSize()
    img_spacing = initial_img.GetSpacing()
    img_pixel_ID = img.GetPixelID()

    # Cast to float to enable bias correction to be used
    image = sitk.Cast(img, sitk.sitkFloat64)

    image = sitk.GetArrayFromImage(image)
    image[image == 0] = np.finfo(float).eps
    image = sitk.GetImageFromArray(image)

    # reset the origin and direction to what it was initially
    image.SetOrigin(initial_img.GetOrigin())
    image.SetDirection(initial_img.GetDirection())
    image.SetSpacing(initial_img.GetSpacing())

    maskImage = sitk.OtsuThreshold(image, 0, 1)

    # Calculating a shrink factor that will be used to reduce image size and increase N4BC speed
    shrink_factor = [(img_size[0] // 64 if img_size[0] % 128 is not img_size[0] else 1),
                     (img_size[1] // 64 if img_size[1] % 128 is not img_size[1] else 1),
                     (img_size[2] // 64 if img_size[2] % 128 is not img_size[2] else 1)]

    # shrink the image and the otsu masked filter
    shrink_filter = sitk.ShrinkImageFilter()
    image_shr = shrink_filter.Execute(image, shrink_factor)
    maskImage_shr = shrink_filter.Execute(maskImage, shrink_factor)

    # apply image bias correction using N4 bias correction
    corrector = sitk.N4BiasFieldCorrectionImageFilter()
    corrected_image_shr = corrector.Execute(image_shr, maskImage_shr)

    # extract the bias field by dividing the shrunk image by the corrected shrunk image
    exp_logBiasField = image_shr / corrected_image_shr

    # resample the bias field to match original image
    reference_image2 = sitk.Image(img_size, exp_logBiasField.GetPixelIDValue())
    reference_image2.SetOrigin(initial_img.GetOrigin())
    reference_image2.SetDirection(initial_img.GetDirection())
    reference_image2.SetSpacing(img_spacing)
    resampled_exp_logBiasField = sitk.Resample(exp_logBiasField, reference_image2)

    # extract the corrected image by dividing the initial image by the resampled bias field that was calculated earlier
    divide_filter2 = sitk.DivideImageFilter()
    corrected_image = divide_filter2.Execute(image, resampled_exp_logBiasField)

    # cast back to initial type to allow for further processing
    corrected_image = sitk.Cast(corrected_image, img_pixel_ID)

    return corrected_image
Example #10
0
def mask(image: sitk.Image,
         mask: sitk.Image,
         jacobian: bool = False) -> sitk.Image:
    r""" Mask an image (special meaning for Jacobian maps).

    Parameters
    ----------
    image : sitk.Image
        Input image to be masked (possibly float).
    mask : sitk.Image
        Mask (possibly float).
    jacobian : bool
        If true, the background after masking is set to
        one, if false to zero.

    Returns
    -------
    sitk.Image
        The masked image.
    """

    if jacobian:
        background = np.logical_not(
            sitk.GetArrayViewFromImage(mask)).astype(np_float_type)
        background = sitk.GetImageFromArray(background)
        background = sitk.Cast(background, image.GetPixelID())
        background.CopyInformation(image)

        cast_mask = sitk.Cast(mask, image.GetPixelID())
        cast_mask.CopyInformation(image)

        result = sitk.Multiply(image, cast_mask)
        result = sitk.Add(result, background)

    else:
        cast_mask = sitk.Cast(mask, image.GetPixelID())
        cast_mask.CopyInformation(image)
        result = sitk.Multiply(image, cast_mask)

    return result
Example #11
0
def supervised_register_images(fixed_image: sitk.Image,
                               moving_image: sitk.Image,
                               initial_transform: sitk.Transform = None,
                               moving_path=None,
                               registration_parameters: dict = None):
    """Register two images
    
        :param fixed_image: image that is being registered to
        :param moving_image: image that is being transformed and registered
        :param initial_transform: the type of registration/transform, e.g. affine or euler
        :param registration_parameters: dictionary of registration key/value arguments
        :return: Registered image, corresponding transform, metric, and stop
        """

    # todo: Re-enable registering for RGB images

    while True:
        registration_method = define_registration_method(
            registration_parameters)
        fixed_final, moving_final, region_extracted = query_for_changes(
            fixed_image, moving_image, initial_transform, registration_method,
            moving_path)

        reg_plot = RegistrationPlot(fixed_final,
                                    moving_final,
                                    transform=initial_transform)

        (transform, metric,
         stop) = register(fixed_final,
                          moving_final,
                          reg_plot,
                          registration_method=registration_method,
                          initial_transform=initial_transform)

        if region_extracted:
            itkplt.plot_overlay(fixed_image,
                                moving_image,
                                transform,
                                downsample=False)

        if query_good_registration(transform, metric, stop):
            break
        # todo: change registration method query here

    registered_image = sitk.Resample(moving_image, fixed_image, transform,
                                     sitk.sitkLinear, 0.0,
                                     moving_image.GetPixelID())

    meta.copy_relevant_metadata(registered_image, moving_image)
    plt.close('all')

    return registered_image, transform, metric, stop
Example #12
0
def level_set_cut_v2(image: sitk.Image, seed: list,
                     pred_image_name: str) -> (sitk.Image, int):
    assert isinstance(image,
                      sitk.Image) and image.GetPixelID() == sitk.sitkUInt8
    seed = map(lambda each: list(map(int, each)), seed)
    ft = sitk.Image(image.GetSize(), sitk.sitkUInt8)
    ft.CopyInformation(image)

    logger.info(pred_image_name + ' have ' + str(len(list(seed))) +
                ' lesion region(s)')

    stats = sitk.LabelStatisticsImageFilter()

    factor = 1.8
    lsFilter = sitk.ThresholdSegmentationLevelSetImageFilter()
    lsFilter.SetMaximumRMSError(0.02)
    lsFilter.SetNumberOfIterations(500)
    lsFilter.SetCurvatureScaling(.5)
    lsFilter.SetPropagationScaling(1)
    lsFilter.ReverseExpansionDirectionOn()

    ex_flag = False
    for each in seed:
        tmp_seg = sitk.Image(image.GetSize(), sitk.sitkUInt8)
        tmp_seg.CopyInformation(image)
        tmp_seg[each] = 1
        tmp_seg = sitk.BinaryDilate(tmp_seg, 3)
        assert isinstance(tmp_seg, sitk.Image)
        init_ls = sitk.SignedMaurerDistanceMap(tmp_seg,
                                               insideIsPositive=True,
                                               useImageSpacing=True)

        stats.Execute(image, tmp_seg)
        lower_threshold = stats.GetMean(1) - factor * stats.GetSigma(
            1)  # - math.log(stats.GetMean(1))
        upper_threshold = stats.GetMean(1) + factor * stats.GetSigma(
            1)  # + math.log(stats.GetMean(1))
        logger.info('the lower_threshold and upper_threshold :' + \
                    str(lower_threshold) + ' ' + str(upper_threshold))
        if lower_threshold == 0 or upper_threshold == 0:
            logger.warn('Threshold Error. Ignoring...')
            continue
        ex_flag = True
        lsFilter.SetLowerThreshold(lower_threshold)
        lsFilter.SetUpperThreshold(upper_threshold)
        ls = lsFilter.Execute(init_ls, sitk.Cast(image, sitk.sitkFloat32))
        assert isinstance(ls, sitk.Image)
        ft += ls

    if ex_flag == True:
        return ft, 1
    return ft, -1
Example #13
0
def apply_transform_params(fixed_image: sitk.Image, moving_image: sitk.Image,
                           transform_params, transform_type):
    """
        Apply a transform based on a list of parameters associated to that transform
        :param fixed_image: Image whose origin/FOV/Spacing the transform will map to
        :param moving_image: Image the transform is being applied to
        :param transform_params: List of transform parameters
        :param transform_type: A blank transform.
        :return:
        """
    transform_type.SetParameters(transform_params)
    return sitk.Resample(moving_image, fixed_image, transform_type,
                         sitk.sitkLinear, 0.0, fixed_image.GetPixelID())
Example #14
0
    def __init__(self, image: sitk.Image):
        """Initializes a new instance of the ImageInformation class.

        Args:
            image (sitk.Image): The image whose properties to hold.
        """
        self.size = image.GetSize()
        self.origin = image.GetOrigin()
        self.spacing = image.GetSpacing()
        self.direction = image.GetDirection()
        self.dimensions = image.GetDimension()
        self.number_of_components_per_pixel = image.GetNumberOfComponentsPerPixel()
        self.pixel_id = image.GetPixelID()
Example #15
0
def compute_dice(b1: sitk.Image, b2: sitk.Image):

    b2 = sitk.Resample(
        b2,
        b1.GetSize(),
        sitk.Transform(),
        sitk.sitkNearestNeighbor,
        b1.GetOrigin(),
        b1.GetSpacing(),
        b1.GetDirection(),
        0,
        b2.GetPixelID(),
    )
    labstats = sitk.LabelOverlapMeasuresImageFilter()

    labstats.Execute(b1, b2)

    return labstats.GetDiceCoefficient()
Example #16
0
def blend_images(fixed_sitk: sitk.Image,
                 moving_sitk: sitk.Image,
                 transform: sitk.Transform = None) -> sitk.Image:
    if not transform:
        transform = sitk.Transform()

    moving_sitk_resampled = sitk.Resample(
        moving_sitk,
        fixed_sitk,
        transform,
        sitk.sitkLinear,
        0.0,
        moving_sitk.GetPixelID(),
    )

    resulting_image = sitk.Compose(moving_sitk_resampled, fixed_sitk,
                                   fixed_sitk)  # red and cyan colors

    return resulting_image
Example #17
0
    def execute(self, image: sitk.Image, params: fltr.IFilterParams=None) -> sitk.Image:
        """Executes a command line program.

        Args:
            image (sitk.Image): The image.
            params (fltr.IFilterParams): The parameters (unused).

        Returns:
            sitk.Image: The filtered image.
        """
        temp_dir = tempfile.gettempdir()
        temp_in = os.path.join(temp_dir, 'in.nii')
        sitk.WriteImage(image, temp_in)
        temp_out = os.path.join(temp_dir, 'out.nii')
        subprocess.run([self.executable_path, temp_in, temp_out], check=True)
        out_image = sitk.ReadImage(temp_out, image.GetPixelID())
        # clean up
        os.remove(temp_in)
        os.remove(temp_out)
        return out_image
Example #18
0
    def __init__(self, image: sitk.Image):
        """Represents ITK image properties.

        Holds common ITK image meta-data such as the size, origin, spacing, and direction.

        See Also:
            SimpleITK provides `itk::simple::Image::CopyInformation`_ to copy image information.

        .. _itk::simple::Image::CopyInformation:
            https://itk.org/SimpleITKDoxygen/html/classitk_1_1simple_1_1Image.html#afa8a4757400c414e809d1767ee616bd0

        Args:
            image (sitk.Image): The image whose properties to hold.
        """
        self.size = image.GetSize()
        self.origin = image.GetOrigin()
        self.spacing = image.GetSpacing()
        self.direction = image.GetDirection()
        self.dimensions = image.GetDimension()
        self.number_of_components_per_pixel = image.GetNumberOfComponentsPerPixel()
        self.pixel_id = image.GetPixelID()
Example #19
0
def minkowski_compactness(image: sitk.Image,
                          norm: Union[int, str] = 1.0) -> float:
    r""" Compute the Minkovski compactness of a binary image.

    Minkowski compactness of an object is defined as the ratio between the volume
    of the object and the volume of a p-ball centred on the object's centre of
    mass, maximised agaist spatial rotations of the ball.

    Parameters
    ----------
    image : sitk.Image
        Input binary image.
    norm : Union[int,str]
        Order of the Minkowski norm ('inf' or 'max' to use the Chebyshev norm).

    Returns
    -------
    float
        Value of the Minkowski norm.
    """

    if image.GetPixelID() != sitk.sitkUInt8:
        raise Exception('Unsupported %s pixel type' %
                        image.GetPixelIDTypeAsString())

    if norm == 'inf' or norm == 'max':
        descriptor = 'cubeness'
    elif norm == 1.0:
        descriptor = 'octahedroness'
    else:
        raise Exception('Unsupported value %s for the norm parameter' %
                        str(norm))

    lssif = sitk.LabelShapeStatisticsImageFilter()
    lssif.Execute(image)

    a = sitk.GetArrayViewFromImage(image) > 0

    return _disptools.shape_descriptor(a, lssif.GetCentroid(1),
                                       image.GetSpacing(), descriptor)
Example #20
0
 def get_reference_image(
     floating_sitk: sitk.Image,
     spacing: TypeTripletFloat,
 ) -> sitk.Image:
     old_spacing = np.array(floating_sitk.GetSpacing())
     new_spacing = np.array(spacing)
     old_size = np.array(floating_sitk.GetSize())
     new_size = old_size * old_spacing / new_spacing
     new_size = np.ceil(new_size).astype(np.uint16)
     new_size[old_size == 1] = 1  # keep singleton dimensions
     new_origin_index = 0.5 * (new_spacing / old_spacing - 1)
     new_origin_lps = floating_sitk.TransformContinuousIndexToPhysicalPoint(
         new_origin_index)
     reference = sitk.Image(
         new_size.tolist(),
         floating_sitk.GetPixelID(),
         floating_sitk.GetNumberOfComponentsPerPixel(),
     )
     reference.SetDirection(floating_sitk.GetDirection())
     reference.SetSpacing(new_spacing.tolist())
     reference.SetOrigin(new_origin_lps)
     return reference
Example #21
0
def sphericity(image: sitk.Image) -> float:
    r""" Measure the sphericity of a object.

    The sphericity is defined [5]_ [6]_ as the ratio between the surface of a sphere
    with the same volume of the object and the surface of the object itself.

    .. math::
        \frac{\pi^{\frac{1}{3}}(6V)^{\frac{2}{3}}}{A}

    A sphere has sphericity equal to 1, non-spherical objects have sphericity
    strictly lesser than 1.

    References
    ----------
    .. [5] Wadell, Hakon. "Volume, shape, and roundness of quartz particles."
           The Journal of Geology 43.3 (1935): 250-280.
    .. [6] Lehmann, Gaëthan. "Label object representation and manipulation with ITK"
           Insight Journal, July-December 2007

    Parameters
    ----------
    image : sitk.Image
        Input binary (sitkUInt8) image.

    Returns
    -------
    float
        A floating point value of sphericity in the interval [0, 1].
    """

    if image.GetPixelID() != sitk.sitkUInt8:
        raise Exception('Unsupported %s pixel type' %
                        image.GetPixelIDTypeAsString())

    lssif = sitk.LabelShapeStatisticsImageFilter()
    lssif.Execute(image)
    return lssif.GetRoundness(1)
Example #22
0
def resample(fixed_image: sitk.Image,
             moving_image: sitk.Image,
             transform: sitk.Transform = None,
             *,
             fusion=False,
             projection=False,
             combine=False,
             invert=False
             ) -> sitk.Image:
    """Resample fixed_image onto the coordinates of moving_image with transform results from registration.

    The registration process results in a transform which maps points from the coordinates of the fixed_image to the
    moving_image. This method is next used to resample the the fixed_image with the transform to produce an image with
    the fixed_image aligned with the moving_image.

    If no transform is provided, then an identity transform is assumed and the moving_image is still resampled onto the
    fixed_image. This operation is useful to see alignment of images before registration.



    :param fixed_image: A 3D SimpleITK Image whose pixel values are resampled
    :param moving_image: A 3D SimpleITK Image whose coordinates are used for the output image
    :param transform: (optional) A 3D SimpleITK Transform mapping from points from the fixed_image to the moving_image.
    :param fusion: Enable fusing the resampled moving_image and the fixed_image into a RGB image.
    :param projection: Enable perform a z-projection to reduce the dimensionality to 2D.
    :param combine: Enable combining the resampled moving_image and the fixed_image into a 2-channel vector image.
    :param invert: Invert the input transform.
    :return: The processed SimpleITK Image.
    """

    if not transform:
        transform = sitk.Transform(3, sitk.sitkIdentity)

    if invert:
        transform = transform.GetInverse()

    if fusion or combine:

        _logger.info("Fusing images...")
        resampled_image = _combine_images(fixed_image, moving_image, transform, fusion=fusion)

    else:
        output_pixel_type = moving_image.GetPixelID()

        _logger.info("Resampling image...")

        resampler = sitk.ResampleImageFilter()
        resampler.SetOutputDirection(fixed_image.GetDirection())
        resampler.SetOutputOrigin(fixed_image.GetOrigin())
        resampler.SetOutputSpacing(fixed_image.GetSpacing())
        resampler.SetSize(fixed_image.GetSize())
        resampler.SetOutputPixelType(output_pixel_type)
        resampler.SetDefaultPixelValue(0)
        resampler.SetInterpolator(sitk.sitkLinear)
        resampler.SetTransform(transform)

        resampled_image = resampler.Execute(moving_image)

    if projection:
        _logger.info("Projecting image...")
        proj_size = resampled_image.GetSize()[:2] + (0,)
        output_pixel_type = resampled_image.GetPixelID()
        projection_image = sitk.Cast(sitk.MeanProjection(resampled_image, projectionDimension=2), output_pixel_type)
        resampled_image = sitk.Extract(projection_image,
                                       size=proj_size,
                                       directionCollapseToStrategy=sitk.ExtractImageFilter.DIRECTIONCOLLAPSETOIDENTITY)

    return resampled_image
Example #23
0
def registration(fixed_image: sitk.Image,     # noqa: C901
                 moving_image: sitk.Image,
                 *,
                 do_fft_initialization=True,
                 do_affine2d=False,
                 do_affine3d=True,
                 ignore_spacing=True,
                 sigma=1.0,
                 auto_mask=False,
                 samples_per_parameter=5000,
                 expand=None) -> sitk.Transform:
    """Robust multi-phase registration for multi-panel confocal microscopy images.

    The fixed and moving image are expected to be the same molecular labeling, and the same imaged regioned.

    The phase available are:
      - fft initialization for translation estimation
      - 2D affine which can correct rotational acquisition problems, this phase is done on z-projections to optimize a \
       2D similarity transform followed by 2D affine
      - 3D affine robust mulit-level registration


    :param fixed_image: a scalar SimpleITK 3D Image
    :param moving_image: a scalar SimpleITK 3D Image
    :param do_fft_initialization: perform FFT based cross correlation for initialize translation
    :param do_affine2d: perform registration on 2D images from z-projection
    :param do_affine3d: multi-level affine transform
    :param ignore_spacing: internally adjust spacing magnitude to be near 1 to avoid numeric stability issues with \
    micro sized spacing
    :param sigma: scalar to change the amount of Gaussian smoothing performed
    :param auto_mask: ignore zero valued pixels connected to the image boarder
    :param samples_per_parameter: the number of image samples to used per transform parameter at full resolution
    :param expand: Perform super-sampling to increase number of z-slices by an integer factor. Super-sampling is \
    automatically performed when the number of z-slices is less than 5.
    :return: A SimpleITK transform mapping points from the fixed image to the moving. This may be a CompositeTransform.

    """
    # Identity transform will be returned if all registration steps are disabled by
    # the calling function.
    result = sitk.Transform()

    initial_translation_3d = True

    moving_mask = None
    fixed_mask = None

    number_of_samples_per_parameter = samples_per_parameter

    expand_factors = None

    if expand:
        expand_factors = [1, 1, expand]

    if fixed_image.GetPixelID() != sitk.sitkFloat32:
        fixed_image = sitk.Cast(fixed_image, sitk.sitkFloat32)

    # expand the image if at least 5 in any dimension
    if not expand_factors:
        expand_factors = [-(-5//s) for s in fixed_image.GetSize()]

    if any([e != 1 for e in expand_factors]):
        _logger.warning("Fixed image under sized in at lease one dimension!"
                        "\tApplying expand factors {0} to image size.".format(expand_factors))
        fixed_image = sitk.Expand(fixed_image, expandFactors=expand_factors)

    if moving_image.GetPixelID() != sitk.sitkFloat32:
        moving_image = sitk.Cast(moving_image, sitk.sitkFloat32)

    expand_factors = [-(-5//s) for s in moving_image.GetSize()]
    if any([e != 1 for e in expand_factors]):
        _logger.warning("WARNING: Moving image under sized in at lease one dimension!"
                        "\tApplying expand factors {0} to image size.".format(expand_factors))
        moving_image = sitk.Expand(moving_image, expandFactors=expand_factors)

    if auto_mask:
        fixed_mask = imgf.make_auto_mask(fixed_image)
        moving_mask = imgf.make_auto_mask(moving_image)

    if ignore_spacing:

        #
        # FORCE THE SPACING magnitude to be normalized near 1.0
        #

        spacing_magnitude = imgf.spacing_average_magnitude(fixed_image)

        _logger.info("Adjusting image spacing by {0}...".format(1.0/spacing_magnitude))

        new_spacing = [s/spacing_magnitude for s in fixed_image.GetSpacing()]
        _logger.info("\tFixed Image Spacing: {0}->{1}".format(fixed_image.GetSpacing(), new_spacing))
        fixed_image.SetSpacing(new_spacing)
        fixed_image.SetOrigin([o/spacing_magnitude for o in fixed_image.GetOrigin()])

        new_spacing = [s / spacing_magnitude for s in moving_image.GetSpacing()]
        _logger.info("\tMoving Image Spacing: {0}->{1}".format(moving_image.GetSpacing(), new_spacing))
        moving_image.SetSpacing(new_spacing)
        moving_image.SetOrigin([o/spacing_magnitude for o in moving_image.GetOrigin()])

        if moving_mask:
            moving_mask.SetSpacing(new_spacing)
            moving_mask.SetOrigin([o/spacing_magnitude for o in moving_mask.GetOrigin()])

        if fixed_mask:
            fixed_mask.SetSpacing(new_spacing)
            fixed_mask.SetOrigin([o / spacing_magnitude for o in fixed_mask.GetOrigin()])

    #
    #
    # Do FFT based translation initialization
    #
    #
    initial_translation = None
    if do_fft_initialization:
        initial_translation = imgf.fft_initialization(moving_image,
                                                      fixed_image,
                                                      bin_shrink=8,
                                                      projection=(not initial_translation_3d))
        result = sitk.TranslationTransform(len(initial_translation), initial_translation)

    #
    # Do 2D registration first
    #
    if do_affine2d:
        result = register_as_2d_affine(fixed_image, moving_image,
                                       sigma_base=sigma,
                                       initial_translation=initial_translation,
                                       fixed_image_mask=fixed_mask,
                                       moving_image_mask=moving_mask)

    if do_affine3d:

        _logger.info("Initializing Affine Registration...")

        if do_affine2d:
            # set the FFT xcoor initial z translation
            if do_fft_initialization and len(initial_translation) >= 3:

                # take the z-translation from the FFT
                translation = list(result.GetTranslation())
                translation[2] = initial_translation[2]
                result.SetTranslation(translation)

                _logger.info("Initialized 3D affine with z-translation... {0}".format(translation))

            affine = result
        else:
            affine = sitk.CenteredTransformInitializer(fixed_image,
                                                       moving_image,
                                                       sitk.AffineTransform(3),
                                                       sitk.CenteredTransformInitializerFilter.GEOMETRY)
            affine = sitk.AffineTransform(affine)

            if do_fft_initialization:
                if len(initial_translation) >= 3:
                    affine.SetTranslation(list(initial_translation))
                    _logger.info("Initialized 3D affine with z-translation... {0}".format(initial_translation))
                else:
                    affine.SetTranslation(list(initial_translation)+[0, ])

        affine_result = register_3d(fixed_image, moving_image,
                                    initial_transform=affine,
                                    sigma_base=sigma,
                                    fixed_image_mask=fixed_mask,
                                    moving_image_mask=moving_mask,
                                    number_of_samples_per_parameter=number_of_samples_per_parameter)

        result = affine_result

    if ignore_spacing:

        # Compose the scaling Transform into a single affine transform

        # The spacing of the image was modified to do registration, so we need to apply the appropriate scaling to
        # transform to the space the registration was done in.r

        scale = spacing_magnitude

        scale_transform = sitk.ScaleTransform(3)
        scale_transform.SetScale([scale]*3)

        result = sitk.CompositeTransform([sitk.Transform(scale_transform),
                                          result,
                                          scale_transform.GetInverse()])

        # if result was a composite transform then we have nested composite
        # transforms white need to be flattened for writing.

        result.FlattenTransform()

        _logger.info(result)

    return result
Example #24
0
    def write(self, segmentation: sitk.Image,
              source_images: List[pydicom.Dataset]) -> pydicom.Dataset:
        """Writes a DICOM-SEG dataset from a segmentation image and the
        corresponding DICOM source images.

        Args:
            segmentation: A `SimpleITK.Image` with integer labels and a single
                component per spatial location.
            source_images: A list of `pydicom.Dataset` which are the
                source images for the segmentation image.

        Returns:
            A `pydicom.Dataset` instance with all necessary information and
            meta information for writing the dataset to disk.
        """
        if segmentation.GetDimension() != 3:
            raise ValueError("Only 3D segmentation data is supported")

        if segmentation.GetNumberOfComponentsPerPixel() > 1:
            raise ValueError("Multi-class segmentations can only be "
                             "represented with a single component per voxel")

        if segmentation.GetPixelID() not in [
                sitk.sitkUInt8,
                sitk.sitkUInt16,
                sitk.sitkUInt32,
                sitk.sitkUInt64,
        ]:
            raise ValueError("Unsigned integer data type required")

        # TODO Add further checks if source images are from the same series
        slice_to_source_images = self._map_source_images_to_segmentation(
            segmentation, source_images)

        # Compute unique labels and their respective bounding boxes
        label_statistics_filter = sitk.LabelStatisticsImageFilter()
        label_statistics_filter.Execute(segmentation, segmentation)
        unique_labels = set(
            [x for x in label_statistics_filter.GetLabels() if x != 0])
        if len(unique_labels) == 0:
            raise ValueError("Segmentation does not contain any labels")

        # Check if all present labels where declared in the DICOM template
        declared_segments = set(
            [x.SegmentNumber for x in self._template.SegmentSequence])
        missing_declarations = unique_labels.difference(declared_segments)
        if missing_declarations:
            missing_segment_numbers = ", ".join(
                [str(x) for x in missing_declarations])
            message = (
                f"Skipping segment(s) {missing_segment_numbers}, since their "
                "declaration is missing in the DICOM template")
            if not self._skip_missing_segment:
                raise ValueError(message)
            logger.warning(message)
        labels_to_process = unique_labels.intersection(declared_segments)
        if not labels_to_process:
            raise ValueError("No segments found for encoding as DICOM-SEG")

        # Compute bounding boxes for each present label and optionally restrict
        # the volume to serialize to the joined maximum extent
        bboxs = {
            x: label_statistics_filter.GetBoundingBox(x)
            for x in labels_to_process
        }
        if self._inplane_cropping:
            min_x, min_y, _ = np.min([x[::2] for x in bboxs.values()],
                                     axis=0).tolist()
            max_x, max_y, _ = (
                np.max([x[1::2]
                        for x in bboxs.values()], axis=0) + 1).tolist()
            logger.info(
                "Serializing cropped image planes starting at coordinates "
                f"({min_x}, {min_y}) with size ({max_x - min_x}, {max_y - min_y})"
            )
        else:
            min_x, min_y = 0, 0
            max_x, max_y = segmentation.GetWidth(), segmentation.GetHeight()
            logger.info(
                f"Serializing image planes at full size ({max_x}, {max_y})")

        # Create target dataset for storing serialized data
        result = SegmentationDataset(
            reference_dicom=source_images[0] if source_images else None,
            rows=max_y - min_y,
            columns=max_x - min_x,
            segmentation_type=SegmentationType.BINARY,
        )
        dimension_organization = DimensionOrganizationSequence()
        dimension_organization.add_dimension("ReferencedSegmentNumber",
                                             "SegmentIdentificationSequence")
        dimension_organization.add_dimension("ImagePositionPatient",
                                             "PlanePositionSequence")
        result.add_dimension_organization(dimension_organization)
        writer_utils.copy_segmentation_template(
            target=result,
            template=self._template,
            segments=labels_to_process,
            skip_missing_segment=self._skip_missing_segment,
        )
        writer_utils.set_shared_functional_groups_sequence(
            target=result, segmentation=segmentation)

        # FIX - Use ImageOrientationPatient value from DICOM source rather than the segmentation
        result.SharedFunctionalGroupsSequence[0].PlaneOrientationSequence[
            0].ImageOrientationPatient = source_images[
                0].ImageOrientationPatient

        buffer = sitk.GetArrayFromImage(segmentation)
        for segment in labels_to_process:
            logger.info(f"Processing segment {segment}")

            if self._skip_empty_slices:
                bbox = bboxs[segment]
                min_z, max_z = bbox[4], bbox[5] + 1
            else:
                min_z, max_z = 0, segmentation.GetDepth()
            logger.info(
                "Total number of slices that will be processed for segment "
                f"{segment} is {max_z - min_z} (inclusive from {min_z} to {max_z})"
            )

            skipped_slices = []
            for slice_idx in range(min_z, max_z):
                frame_index = (min_x, min_y, slice_idx)
                frame_position = segmentation.TransformIndexToPhysicalPoint(
                    frame_index)
                frame_data = np.equal(
                    buffer[slice_idx, min_y:max_y, min_x:max_x], segment)
                if self._skip_empty_slices and not frame_data.any():
                    skipped_slices.append(slice_idx)
                    continue

                frame_fg_item = result.add_frame(
                    data=frame_data.astype(np.uint8),
                    referenced_segment=segment,
                    referenced_images=slice_to_source_images[slice_idx],
                )

                frame_fg_item.FrameContentSequence = [pydicom.Dataset()]
                frame_fg_item.FrameContentSequence[0].DimensionIndexValues = [
                    segment,  # Segment number
                    slice_idx - min_z + 1,  # Slice index within cropped volume
                ]
                frame_fg_item.PlanePositionSequence = [pydicom.Dataset()]
                frame_fg_item.PlanePositionSequence[0].ImagePositionPatient = [
                    f"{x:e}" for x in frame_position
                ]

            if skipped_slices:
                logger.info(f"Skipped empty slices for segment {segment}: "
                            f'{", ".join([str(x) for x in skipped_slices])}')

        # Encode all frames into a bytearray
        if self._inplane_cropping or self._skip_empty_slices:
            num_encoded_bytes = len(result.PixelData)
            max_encoded_bytes = (segmentation.GetWidth() *
                                 segmentation.GetHeight() *
                                 segmentation.GetDepth() *
                                 len(result.SegmentSequence) // 8)
            savings = (1 - num_encoded_bytes / max_encoded_bytes) * 100
            logger.info(
                f"Optimized frame data length is {num_encoded_bytes:,}B "
                f"instead of {max_encoded_bytes:,}B (saved {savings:.2f}%)")

        result.SegmentsOverlap = "NO"

        return result
Example #25
0
def cubicity(image: sitk.Image) -> float:
    r""" Measure the cubicity of an object.

    The cubicity is defined [4]_ as the ratio between the volume of the
    object and the volume of its bounding cube.
    A cube has cubicity equal to 1, a sphere has cubicity pi/6, and in
    general non-cubic objects have cubicity strictly lesser than 1.

    Here the bounding cube is estimated as the cube whose side is
    equal to the longest side of the oriented bounding box of the
    object.

    References
    ----------
    .. [4] O'Flannery, LJ and O'Mahony, MM, "Precise shape grading of
           coarse aggregate". Magazine of Concrete Research 51.5 (1999),
           pp. 319-324.

    Parameters
    ----------
    image : sitk.Image
        Input binary (sitkUInt8) image.

    Returns
    -------
    float
        A floating point value of cubicity in the interval [0, 1].
    """

    if image.GetPixelID() != sitk.sitkUInt8:
        raise Exception('Unsupported %s pixel type' %
                        image.GetPixelIDTypeAsString())

    # NOTE: the size of the bounding box is already in image space
    # units, while the volume (number of voxels) needs to be multiplied
    # by the voxel size
    dv = functools.reduce(lambda x, y: x * y, image.GetSpacing(), 1.0)

    try:
        lssif = sitk.LabelShapeStatisticsImageFilter()
        lssif.ComputeOrientedBoundingBoxOn()
        lssif.Execute(image)

        (s1, s2, s3) = lssif.GetOrientedBoundingBoxSize(1)
        volume = lssif.GetNumberOfPixels(1) * dv

    except AttributeError:
        # Use ITK as a fallback if the method is not available in
        # SimpleITK

        if 'itk' not in sys.modules:
            raise Exception(
                'sitk_to_itk: itk module is required to use this feature.')

        itk_image = drawing.sitk_to_itk(image)
        li2slmf = itk.LabelImageToShapeLabelMapFilter.IUC3LM3.New(itk_image)
        li2slmf.ComputeOrientedBoundingBoxOn()

        statistics = li2slmf()[0][1]

        #  FIXME GetOrientedBoundingBoxSize() seems to be broken
        #  (s1, s2, s3) = statistics.GetOrientedBoundingBoxSize() #
        (s1, s2, s3) = statistics.GetBoundingBox().GetSize()

        # FIXME GetBoundingBox is in voxels, GetOrientedBoundingBox is
        # in image size instead, so multiply by the spacing when using
        # GetBoundingBox
        (s1, s2,
         s3) = [x * s for x, s in zip([s1, s2, s3], image.GetSpacing())]

        volume = statistics.GetNumberOfPixels() * dv

    return volume / (max(s1, s2, s3)**3)